背景
有一些应用系统或应用功能,如日程管理、任务管理需要使用到日历组件。虽然Element Plus也提供了日历组件,但功能比较简单,用来做数据展现勉强可用。但如果需要进行复杂的数据展示,以及互动操作如通过点击添加事件,则需要做大量的二次开发。
FullCalendar是一款备受欢迎的开源日历组件,以其强大的功能而著称。其基础功能不仅免费且开源,为开发者提供了极大的便利,仅有少量高级功能需要收费。然而,尽管该组件功能卓越,其文档却相对简洁,导致在集成过程中需要开发者自行摸索与探索,这无疑增加了不少学习和验证的时间成本。
为此,本专栏通过日程管理系统的真实案例,手把手带你了解该组件的属性和功能,通过需求导向的方式,详细阐述FullCalendar组件的集成思路和实用解决方案。
在介绍过程中,我们将重点关注集成要点和注意事项,力求帮助开发者在集成过程中少走弯路,提供有效的避坑指南,从而提升开发效率,更好地利用这款优秀的日历组件。
官网:https://fullcalendar.io/
环境Vue3+Element Plus+FullCalendar 6.1.11。
使用
按需加载数据(五)
回顾下前文,我们自定义扩展了显示全部任务还是只显示未结束的任务,因FullCalendar自身组件功能限制,切换时,通过刷新当前页面来实现。在刷新操作处理中,调用了tab页刷新,将当前用户操作的视图类型和是否显示全部的标识位以query参数的方式进行传递。
// 刷新
refresh() {const fullCalendar = this.$refs.fullCalendar.calendarlet query = this.$route.queryquery = Object.assign(query, {viewType: fullCalendar.view.type,showAllFlag: this.showAllFlag})refreshSelectedTagWithQuery(query)
}
在页面初始化时,从query参数中获取到视图类型,调用FullCalendar组件的内置changeView方法来切换视图。
// 初始化init() {this.calendarApi = this.$refs.fullCalendar.getApi()// 处理是否显示全部if (this.$route.query.showAllFlag != undefined) {//此处注意,query参数是字符串类型,直接赋值给showAllFlag会令其类型变化,使用非运算符!会一直为truethis.showAllFlag = this.$route.query.showAllFlag == 'true' ? true : false}// 默认设置视图类型let viewType = this.calendarOptions.initialView// query参数中取值if (this.$route.query.viewType) {viewType = this.$route.query.viewType}// 调用日历组件api实现视图切换const fullCalendar = this.$refs.fullCalendar.calendarfullCalendar.changeView(viewType)}
当时测试时查看后端情况,发现还有个小瑕疵,就是页面加载的时候,实际会触发两次调用后端服务请求,推测一次是来源于组件自身初始化加载,另一次是我们手工调用的api方法来切换视图。当时觉得只有在切换显示范围时才会触发,影响很小,先搁置了。
在使用过程中,这两次加载产生了严重的问题,表现为加载或刷新页面时,会出现事件不显示的情况。经深入分析和排查,问题就出在两次加载上。
举例说明,假设FullCalendar的默认视图initialView属性设置为日视图timeGridDay。
用户第一次访问页面,FullCalendar组件加载时会触发一次视图,然后我们又调用了一次切换视图,在加载事件数据的方法中打印,查看控制台和后端请求,确实调用了两次。
// 加载事件数据
loadEvent(fetchInfo, successCallback, failureCallback) {console.log('loadEvent')this.startTime = this.$dateFormatter.formatUTCTime(fetchInfo.start)this.endTime = this.$dateFormatter.formatUTCTime(fetchInfo.end)this.successCallback = successCallbackthis.loadData()
}
这一点我们可以优化,即判断当前视图与要切换的目标视图是否同一个,相同则不再调用。
// 初始化
init() {this.calendarApi = this.$refs.fullCalendar.getApi()// 处理是否显示全部if (this.$route.query.showAllFlag != undefined) {//此处注意,query参数是字符串类型,直接赋值给showAllFlag会令其类型变化,使用非运算符!会一直为truethis.showAllFlag = this.$route.query.showAllFlag == 'true' ? true : false}// 默认设置视图类型let viewType = this.calendarOptions.initialView// query参数中取值if (this.$route.query.viewType) {viewType = this.$route.query.viewType}// 当前视图与要切换的目标视图不是同一个时,调用日历组件api实现视图切换if (viewType != this.calendarOptions.initialView) {const fullCalendar = this.$refs.fullCalendar.calendarfullCalendar.changeView(viewType)}
}
调整后查看控制台,只输出一次了。
在完成页面首次访问后,用户点击顶部右侧按钮“月”、“周”、“列表”切换视图,都能正常显示。
当用户切换到周视图后,点击查看范围切换按钮时,这时候实际进行了页面重新加载,查看后端调用了两次数据查询服务。因为初始化视图是日视图,最后要展现的是周视图。一次来源于FullCalendar的初始化,一次来源于我们手工调用api方法。这时候发生的问题就在于,这两次数据绑定,看上去仍会只会生效一次,有时候绑定的是单天的数据,有时候绑定的是一周的数据,这样会导致在周视图下,因为加载的数据是单天的,周视图中其余六天的数据没有显示,就跟“丢失”一样。
给调用切换视图的api设置了延迟3秒执行,如下所示:
setTimeout(() => {const fullCalendar = this.$refs.fullCalendar.calendarfullCalendar.changeView(viewType)
}, 3000)
然后测试,发现数据绑定和加载正常了,只是3秒的延迟对用户体验很大。
基于上述验证思考,推测问题的根源我们使用了“全局”变量successCallback缓存了回调方法,两次视图加载执行时间非常接近,该变量被错误的赋值。
动手尝试,去除全局变量successCallback(data根目录下变量),将加载事件数据中的successCallback的值通过方法参数传递,如下:
// 加载事件数据loadEvent(fetchInfo, successCallback, failureCallback) {console.log('loadEvent')this.startTime = this.$dateFormatter.formatUTCTime(fetchInfo.start)this.endTime = this.$dateFormatter.formatUTCTime(fetchInfo.end)this.loadData(successCallback)},// 加载数据loadData(successCallback) {this.$api.personaltask.task.listWithScope(this.startTime, this.endTime).then((res) => {if (res.data) {const eventArray = res.data.map((item) => {// 若起止时间均为00:00:00,则设置为allDay属性为truelet allDay = falseif (item.startTime &&item.endTime &&item.startTime.substr(11, 8) === '00:00:00' &&item.endTime.substr(11, 8) === '00:00:00') {allDay = true}return {id: item.id,title: item.name,start: item.startTime,end: item.endTime,allDay: allDay,status: item.status,extendedProps: {priority: item.priority,plannedDuration: item.plannedDuration}}})this.eventData = eventArraythis.filteData(successCallback)}})},// 筛选数据filteData(successCallback) {if (this.showAllFlag == true) {this.calendarOptions.customButtons.changeShowScopeButton.text = '显示未结束'successCallback(this.eventData)} else {this.calendarOptions.customButtons.changeShowScopeButton.text = '显示全部'const filtedData = this.eventData.filter((item) => {return (item.status === 'IN_PROGRESS' ||item.status === 'TO_DO' ||item.status === 'EXPIRED' ||item.status === 'PENDING' ||item.status === 'PAUSED')})successCallback(filtedData)}},
测试结果终于正常了。