拖放外部元素至FullCalendar组件内
业务场景
先来说下需求。
我们通过日历组件来安排日程,主要是安排那些有相对明确的时间的事项。但实际还存在一些事项,比如临时的,刚想起或刚产生的,不紧急,甚至需要完成一些前置依赖。安排到哪个时间点尚不确定。
这种场景下,需要一个收集箱来暂存这些任务,用于提醒,防止遗忘而遗漏。
如上所述,将任务状态标记为“待安排”,并将其起止时间值为空。
安排计划时再根据实际情况把这些事项放到具体的计划中。
功能设计
对于上述需求,我们在当前日历组件之外,增加收集箱的功能,用于存放和显示“待安排”的任务,如下图所示:
这样用户不需要切换到别的菜单来新建待安排的事项,并且在安排计划时候也可以将待安排的事项一并考虑。
虽然用户可以在收集箱中的任务清单中点击某项任务,设置起止时间,通过刷新日历组件来显示,但是操作较繁琐。为了便于操作,我们来实现“拖放”操作,即将左侧收集箱中的任务,直接拖放到日历组件中,根据放置的位置自动设置起止时间,这样做更方便与直观。
即我们需要将一个FullCalendar日历组件外的元素,拖放到日历组件内部。
尝试方案
官网说明文档有相关描述,但是过于简略,需要摸索。
首先,需要配置属性,将拖放功能droppable开关打开:
// 启用拖动外部元素放置到日历
droppable: true
然后,定义拖放结束的回调事件drop,注意不是前面用过的eventDrop,如下:
//外部元素拖放到日历中
drop: this.drop
接下来,最关键的操作就是外部元素拖拽到日历组件内部了。
官网描述的使用draggable,说的很含糊,这个draggable到底指啥并不明确。
一开始,尝试使用自己熟悉的vuedraggable组件来实现拖拽功能,然后测试失败,drop回调事件触发不了,怀疑被vuedraggable自己截获和处理了。
然后,使用html5自身的draggable,同样发现无法触发回调事件。
解决方案
回过头细看官方说明,原来导入的是FullCalendar自身的Draggable组件,但是写法看上去又很奇怪,是JQuery模式的操作,如下:
document.addEventListener('DOMContentLoaded', function() {
var Calendar = FullCalendar.Calendar;
var Draggable = FullCalendar.Draggable;
var containerEl = document.getElementById('external-events');
var calendarEl = document.getElementById('calendar');
var checkbox = document.getElementById('drop-remove');
// initialize the external events
// -----------------------------------------------------------------
new Draggable(containerEl, {
itemSelector: '.fc-event'
});
// initialize the calendar
// -----------------------------------------------------------------
var calendar = new Calendar(calendarEl, {
headerToolbar: {
left: 'prev,next today',
center: 'title',
right: 'dayGridMonth,timeGridWeek,timeGridDay'
},
editable: true,
droppable: true, // this allows things to be dropped onto the calendar
drop: function(info) {
console.log(info)
// is the "remove after drop" checkbox checked?
if (checkbox.checked) {
// if so, remove the element from the "Draggable Events" list
info.draggedEl.parentNode.removeChild(info.draggedEl);
}
}
});
calendar.render();
});
尝试将其转化为vue的写法。
引入draggable组件:
import { Draggable } from '@fullcalendar/interaction'
使用div包裹,指定id为external-events,任务列表通过el-row附加v-for,指定class为dragElement,如下:
<div id="external-events">
<el-row
v-for="element in taskList"
:key="element.id"
class="dragElement"
>
<el-tag
closable
@close="remove(element.id)"
@click="modify(element.id)"
:title="element.name"
>
{{ element.name }}</el-tag
>
</el-row>
</div>
初始化加载数据的操作里,调用Draggable,这时候,需要通过 document.getElementById(‘external-events’)获取外层的div元素,并且通过itemSelector: '.dragElement’来获取可拖拽的元素,如下:
// 初始化
init() {
this.loadData()
},
loadData() {
this.$api.personaltask.task.listWithStatus('PENDING').then((res) => {
if (res.data) {
this.taskList = res.data
let containerEl = document.getElementById('external-events')
new Draggable(containerEl, {
itemSelector: '.dragElement',
eventData: function (eventEl) {
return {
title: eventEl.innerText,
duration: '00:30'
}
}
})
}
})
}
完成上述操作后,可以实现任务从外部拖放到日历组件内部了,当然,仅限于前端。
接下来,就是调用后端服务,将被拖动的任务,根据放到日历组件中的位置,设置其起止时间,并变更其状态为“未开始”。
drop回调事件中,能拿到几个重要属性如下:
allDay:是否全天事件
date:拖放结束放置的位置所在的时间
draggedEl:被拖放的html元素
我们可以通过date获取开始时间,然后结合allDay来计算结束时间,如果是全天,开始时间+1天为结束时间;如果是非全天,开始时间+半小时(我们自行设置的默认持续时长)。
还有最关键的一点,是如何获取到被拖拽的任务的标识id,通过打印输出的方式,发现draggedEl元素里仅传递了任务标题,并没有传递id。
尝试了很多方式,最终是使用date-id这种定义属性,来放入任务标识:
<div id="external-events">
<el-row
v-for="element in taskList"
:key="element.id"
class="dragElement"
:data-id="element.id"
>
<el-tag
closable
@close="remove(element.id)"
@click="modify(element.id)"
:title="element.name"
>
{{ element.name }}</el-tag
>
</el-row>
</div>
然后在drop的回调方法中,通过arg.draggedEl.dataset.id的方式取出来,整个回调方法如下:
drop(arg) {
// 获取是否全天
const allDay = arg.allDay
// 获取开始时间
const start = arg.date
// 获取任务标识
const id = arg.draggedEl.dataset.id
let endTime = new Date(start)
if (allDay) {
//若为全天,结束时间为开始时间加1天
endTime = this.$dateFormatter.formatUTCTime(new Date(start.getTime() + 24 * 60 * 60 * 1000))
} else {
// 非全天,结束时间在开始时间基础上加半小时
endTime = this.$dateFormatter.formatUTCTime(new Date(start.getTime() + 30 * 60 * 1000))
}
const startTime = this.$dateFormatter.formatUTCTime(new Date(start.getTime()))
// 调用安排工作接口
this.$api.personaltask.task.assign(id, startTime, endTime).then(() => {
this.refresh()
})
}
最终,实现了将外部元素拖放到日历组件内部的功能。
重构为无刷新模式
前面我们增加了收集箱功能,用于存放待安排的任务,并实现了从收集箱拖放任务到日历的功能,如下图所示:
使用的是FullCalendar的drop事件,最后调用的是刷新操作,如下:
退回收集
通过右键菜单,可以将某个暂不具备的执行条件的任务退回了收集箱,通过调用FullCalendar的删除任务的api,并调用收集箱的加载数据方法,来实现页面无刷新,如下:
// 设置待安排
setPending(id) {
this.$api.personaltask.task.changeStatus(id, 'PENDING').then(() => {
this.revmoveTask(id)
this.reloadCollectionBox()
})
}
// 删除任务
revmoveTask(taskId) {
const fullCalendar = this.$refs.fullCalendar.calendar
const event = fullCalendar.getEventById(taskId)
event.remove()
}
// 刷新收集箱
reloadCollectionBox() {
this.$refs.collectionBox.loadData()
}
应用系统
名称:遇见
地址:https://meet.popsoft.tech
说明:基于一二三应用开发平台和FullCalendar日历组件实现的面向个人的时间管理、任务管理系统,1分钟注册,完整功能,欢迎使用~