基于Vue3 + FullCalendar实现会议日程预约管理系统
最终效果图
日:
周:
月:
一、 FullCalendar插件说明
1. 官网介绍“The Most Popular JavaScript Calendar” 最受欢迎的JavaScript日历!支持 Vue、React、Angular、JavaScript脚本语言。
官网链接:FullCalendar - JavaScript Event Calendar
2. 使用时请先下载相关插件:
npm i @fullcalendar/vue3
npm i @fullcalendar/core
npm i @fullcalendar/daygrid
npm i @fullcalendar/timegrid
npm i @fullcalendar/interaction
本篇中以上插件均使用 "^6.1.9" 版本
下载后通过import 引用即可
import FullCalendar from '@fullcalendar/vue3'
import dayGridPlugin from '@fullcalendar/daygrid'
import timeGridPlugin from '@fullcalendar/timegrid'
import interactionPlugin from '@fullcalendar/interaction'
二、技术梳理
整体分为以下三部分
1. 左上el-calendar日历部分
当前周的背景色、与FullCalendar日期联动、日历本地化:设置周一为每周的第一天(此处有小坑)。
2. 左下订阅部分
checkbox本身不支持直接修改颜色,通过伪类样式覆盖实现不同颜色展示,再通过修改styleSheet修改checkbox伪类背景色、以及相关业务功能逻辑。
3. 右侧FullCalendar
熟悉fullCalenda相关配置项,按需配置初始视图、语言、固定行数、宽高比等静态结构,自定义周看板,根据相关事件,例如点击事件、滑动选择、拖动事件、等编写相关逻辑。
三、实现方案
1.自定义el-calender
自定义头部,通过绑定dateRef手动切换日期月份,同时调用FullCalendar相关方法,确保两个日历日期保持一致。
<el-calendar v-model="date" ref="dateRef" class="custom-calendar">
<template #header="{ date }">
<div class="w-full flex justify-between">
<span>{{ date }}</span>
<div class="w-20 flex justify-between">
<el-icon class="cursor-pointer" @click="selectDate('prev-month')">
<ArrowLeftBold />
</el-icon>
<el-icon class="cursor-pointer" @click="selectDate('next-month')">
<ArrowRightBold />
</el-icon>
</div>
</div>
</template>
</el-calendar>
// 切换日期月份
const selectDate = value => {
if (!dateRef.value) return
dateRef.value.selectDate(value)
changeDate(date.value)
}
// 同步calendarRef
const changeDate = date => calendarRef.value.getApi().gotoDate(date)
设置周背景色,通过watch监视日期变化,当用户选择日期后实时计算此时的周一和周日的日期,处在两者中的日期,添加样式,完整代码如下
<!-- 日历 -->
<el-calendar v-model="date" ref="dateRef" class="custom-calendar">
<template #header="{ date }">
<div class="w-full flex justify-between">
<span>{{ date }}</span>
<div class="w-20 flex justify-between">
<el-icon class="cursor-pointer" @click="selectDate('prev-month')">
<ArrowLeftBold />
</el-icon>
<el-icon class="cursor-pointer" @click="selectDate('next-month')">
<ArrowRightBold />
</el-icon>
</div>
</div>
</template>
<template #date-cell="{ data }">
<p class="w-full h-full flex items-center justify-center"
:class="[data.date >= selectedWeekRange[0] && data.date <= selectedWeekRange[1] ? 'is-week' : '']"
@click="changeDate(data.date)">
{{ data.day.split('-').slice(2).join() }}
{{ data.isSelected ? '✔️' : '' }}
</p>
</template>
</el-calendar>
// 周背景色
watch(
date,
(newValue) => {
// 获取用户选择的日期
const selectedDate = new Date(newValue);
// 获取用户选择日期是所在周的第几天(周日为0,周一为1,以此类推)
const dayOfWeek = selectedDate.getDay();
// 计算周一的日期
let monday = new Date(selectedDate);
monday.setDate(selectedDate.getDate() - dayOfWeek + (dayOfWeek === 0 ? -6 : 1));
// 计算周日的日期
let sunday = new Date(selectedDate);
sunday.setDate(selectedDate.getDate() + (7 - dayOfWeek) + (dayOfWeek === 0 ? -7 : 0));
selectedWeekRange.value = [monday, sunday];
}, { immediate: true }
)
日历本地化,设置周一为周的第一天
Element Plus官方文档解释说:我们使用 Day.js 库来管理组件的日期和时间,例如DatePicker 。 必须在 Day.js 中设置一个适当的区域,以便使国际化充分发挥作用。 您必须分开导入Day.js的区域设置。
所以我们在main.js中加入如下代码
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import 'dayjs/locale/zh-cn'
理论上这样就可以了,相信大部分人也都可以,但是这里有个坑就是这样可能并不会生效,博主在实验多次后发现,按照如上设置后其他的涵盖日期的组件都会生效,例如日期选择器、时间日期选择器,唯独这个日历不生效.....
最后,需要在上述代码的基础上额外添加
import dayjs from 'dayjs'
dayjs.locale('zh-cn')
往往困扰几天,自认为很复杂,甚至都打算去扒源码的bug,只需要两行代码就能解决...hh
2.订阅功能
功能部分, 主要涉及权限的修改,订阅人的添加与取消,修改颜色,主要为业务逻辑,技术难度相对较低,这里只做展示,不进行展开。
样式部分, 主要涉及FullCalendar事件背景色,以及checkbox复选框颜色覆盖
由于checkbox复选框本身不支持颜色的修改,所以我们使用伪类样式对其进行覆盖
该结构由数据遍历而来,故而我们可以为每个checkbox绑定不同的类名: :class="itemBox_${index}
" ,代码如下
<el-collapse-item title="订阅日程" name="second" class="relative z-50 collapseBox" style="height: auto;">
<div class="w-full h-44 overflow-y-scroll overflow-x-hidden z-50">
<div v-for="(item,index) in subscriptionList" :key="item.id">
<div class="flex justify-between items-center mx-4 mb-2" :class="`itemBox_${index}`" v-if="index != 0">
<div class="flex items-center ">
<input type="checkbox" v-model="item.isChecked" class="mr-2" :class="`checked_${item.id}`" @change="filterEvent(item)">
<p class=" text-base">{{ item.followName }}</p>
</div>
<el-icon size="20" color="#54575D" class="cursor-pointer" @click.stop="getSubscriberLoaction(index)">
<MoreFilled />
</el-icon>
<aside v-show="selectedItem == index" class="absolute shadow-deeper rounded-lg rounded-tr-none overflow-hidden" :class="`asideBox_${index}`" style="width: 140px; max-height: 110px; z-index:999999;" @click.stop="null">
<div class="w-full h-full p-4 pt-5 bg-mainWhite">
<div class="mb-4">
<div class="flex justify-between items-center">
<p class="text-test text-base mb-1 cursor-pointer">修改颜色</p>
<el-color-picker v-model="item.color" show-alpha :predefine="predefineColors" @change="changeColor($event,item)" />
</div>
<p class="text-test text-base mb-1 cursor-pointer" @click="setPermission(item)" v-if="item.editable == 1">设置权限</p>
<div class="relative">
<el-popconfirm title="确认取消订阅?" confirm-button-text="是" cancel-button-text="否" :icon="InfoFilled" icon-color="#626AEF" placement="right" @confirm="handleCancleSubscription(item.id)">
<template #reference>
<p class="text-test text-base mb-1 cursor-pointer">取消订阅</p>
</template>
</el-popconfirm>
</div>
</div>
</div>
</aside>
</div>
</div>
</div>
</el-collapse-item>
这样当我们在获取订阅人数据时,通过styleSheets添加伪类样式,为不同的用户添加颜色
// 正常获取数据,处理数据,储存数据...
// 获取样式表
const styleSheet = document.styleSheets[0];
subscriptionList.value.map(item => {
const rule = `.checked_${item.id}:checked::after {content: "✔" !important; color: ${item.color} ; font-size: 12px; font-weight: bold; border: 2px solid ${item.color}; background-color: white; }`;
// 将生成的规则插入样式表中
styleSheet.insertRule(rule, 0);
})
当用户修改颜色时,我们除了需要向后台发送最新的颜色外,还需要手动更新styleSheets样式(直接刷新用户列表,获取订阅人不会生效,原因为浏览器未刷新,样式表不会更新)
// 修改事件颜色
// 获取最新颜色,发送请求,接口返回200后
// 修改样式
const styleSheet = document.styleSheets[0];
const rules = `.checked_${item.id}:checked::after {content: "✔" !important; color: ${color} !important; font-size: 12px; font-weight: bold; border: 2px solid ${color} !important; background-color: white; }`;
const keys = Object.keys(styleSheet.cssRules);
// 遍历所有样式表,找到原本样式规则进行修改
for (let i = 0; i < keys.length; i++) {
if (styleSheet.cssRules[i].selectorText == `.checked_${item.id}:checked::after`) {
styleSheet.cssRules[i].style.color = `${color} `
styleSheet.cssRules[i].style.border = `2px solid ${color}`
styleSheet.cssRules[i].cssText = rules
// break; 原本是想找到后停止遍历,但是不能写break,会有多条重复的样式表造成不生效
}
}
3.FullCalendar的使用
FullCalendar的内置函数及配置项
// 切换到下一月/周/日
this.$refs.FullCalendar.getApi().next()
// 切换到上一月/周/日
this.$refs.FullCalendar.getApi().prev()
// 跳转到今天
this.$refs.FullCalendar.getApi().today()
// 跳转到指定日期 formatData是日期 格式为 yyyy-MM-dd
this.$refs.FullCalendar.getApi().gotoDate(formatData)
// 获得当前视图起始位置的日期
this.$refs.FullCalendar.getApi().getDate()
// 获得当前视图 里面有一些参数
this.$refs.FullCalendar.getApi().view
// 当前视图的类型
this.$refs.FullCalendar.getApi().view.type
// 当前显示的事件(日程)的开始时
this.$refs.FullCalendar.getApi().view.activeStart
// 当前显示的事件(日程)的结束时
this.$refs.FullCalendar.getApi().view.activeEnd
//访问当前视图所涉及的日历对象或者日历配置信息。
this.$refs.FullCalendar.getApi().view.calendar
// 获得当前所显示的所有事件(日程)
this.$refs.FullCalendar.getApi().view.calendar.getEvents()
// 向日历中添加事项
this.$refs.FullCalendar.getApi().view.calendar.addEvent({
id: '001',
title: `青兔_test01`,
start: '2024-04-25' + ' 13:00:00',
end: '2024-04-25' + ' 17:00:00',
// 修改背景颜色
backgroundColor:'#d8377a',
// 修改边框颜色
borderColor:'#d8377a',
})
更多更详细FullCalendar介绍可查询官方文档,本篇只展示当前功能下所需要的相关配置
官网链接:FullCalendar - JavaScript Event Calendar
配置FullCalendar, 了解相关配置项,本篇中使用的配置如下
// fullCalendar 配置项
const calendarOptions = reactive({
plugins: [dayGridPlugin, interactionPlugin, timeGridPlugin], //需要加载的插件
initialView: "timeGridDay", //初始视图
height: "780px",
locale: zhcn, //语言汉化
selectable: true,
editable: true,
forceEventDuration: true,
// droppable: false,
// dropAccept: ".eventListItems", //可被拖进
dayMaxEventRows: 99, //事件最大展示列数
nowIndicator: true,
fixedWeekCount: false, //因为每月起始日是星期几不固定,导致一个月的行数会不固定,是否固定行数
// drop: null, //外部拖拽进的事件方法
handleWindowResize: true,
windowResizeDelay: 100,
allDaySlot: false, // 关闭全天选项
aspectRatio: 2, //宽高比
// 最小时间
slotMinTime: '06:00:00',
// 最大时间
slotMaxTime: '22:30:00',
customButtons: {
myCustomButton: {
text: '看板',
click: function() {
isWeekViewShow.value = true
}
}
},
headerToolbar: {
left: "today prev next",
center: "title",
right: "myCustomButton,dayGridMonth,timeGridWeek,timeGridDay"
}, //日历上方的按钮和title
events: matchList.value, //绑定展示事件
// 自定义日程展示内容
// eventContent: event => {},
eventDidMount: (info) => {},
//点击日期info是单元格的信息
dateClick: info => {},
//事件的点击
eventClick: info => {},
// 移动事件或者拓展事件时间触发函数 返回数组 item._context.options.events Array 当前所有事件
eventsSet: info => {},
// 滑动选择时触发
select: info => {},
// 时间调整结束后触发
eventResize: info => {},
// 拖动日程触发
eventDrop: info => {},
// 切换视图时触发
datesSet: view => {},
});
同步日期, el-calendar与fullCalendar需要双向绑定,前面点击小日历切换日期时同时修改日程日期,现使用FullCalendar内置函数对小日历进行日期绑定
onMounted(() => {
nextTick(() => {
// calendar日期同步到日历
document.querySelector('.fc-today-button').addEventListener('click', function() {
date.value = calendarRef.value.getApi().getDate()
});
document.querySelector('.fc-prev-button').addEventListener('click', function() {
date.value = calendarRef.value.getApi().getDate()
});
document.querySelector('.fc-next-button').addEventListener('click', function() {
date.value = calendarRef.value.getApi().getDate()
});
// 绑定事件
document.addEventListener('click', handleClickOutside);
})
})
相关业务事件, 例如用户滑动选择、拖拽日程边缘增加/减少时间、拖动日程修改日程在对应方法中执行相关业务逻辑即可,对应的回调传递info参数,在info.event中可以获取用户执行后的开始时间和结束时间,需要强调的是,此时获取的时间为一个表示日期和时间的 ISO 8601 格式的字符串,我们需要把他转换成我们需要的YYYY-MM-DD HH:mm格式,封装formatDateTime方法。
// 格式化日期
const formatDateTime = (isoString) => {
const date = new Date(isoString);
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const day = date.getDate().toString().padStart(2, '0');
const hours = date.getHours().toString().padStart(2, '0');
const minutes = date.getMinutes().toString().padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}`;
}
//点击日期info是单元格的信息
dateClick: info => {},
//事件的点击
eventClick: info => {
// 业务逻辑...
},
eventsSet: info => {},
// 滑动选择时触发
select: info => {
// 处理时间数据
const startDate = formatDateTime(info.start)
const endDate = formatDateTime(info.end)
// 业务逻辑...
},
// 时间调整结束后触发
eventResize: async info => {
resizeEventDate(info)
},
// 拖动日程触发
eventDrop: async info => {
resizeEventDate(info)
},
const resizeEventDate = async info => {
const resizeEvent = matchList.value.find(item => item.id == info.event.id)
resizeEvent.startTime = formatDateTime(info.event.start)
resizeEvent.endTime = formatDateTime(info.event.end)
const result = await submit(resizeEvent)
if (result.code == 200) ElMessage.success('修改成功')
else ElMessage.error('修改失败')
const timeObj = {
startTime: '',
endTime: ''
}
timeObj.startTime = date2Str(info.view.activeStart)
timeObj.endTime = date2Str(info.view.activeEnd)
myMatchList(timeObj) // 修改成功后重新获取数据
}
切换视图, 通过内置方法datesSet中的view参数,可以获得当前视图是日期范围,需要强调的是,返回的结束时间为后一天的零天,例如周时间为2023-12-11 至2023-12-17,实际返回的结果为2023-12-11T00:00:00+08:00" 至 "2023-12-18T00:00:00+08:00", 所以我们需要修改结束时间。
// 切换视图时触发
datesSet: view => {
timeObj.startTime = date2Str(view.start)
// 结束时间返回为后一天的零点 例如周时间应为2023-12-11 至 2023-12-17
// 实际返回结果为2023-12-11T00:00:00+08:00" 至 "2023-12-18T00:00:00+08:00"
// 修改结束时间
const timeTemp = new Date(date2Str(view.end)).getTime() - 86400000 // 减一天后的时间戳
timeObj.endTime = date2Str(new Date(timeTemp))
// 周视图-领导视图交互及数据处理
if ((view.end.getTime() - view.start.getTime()) / 1000 / 3600 / 24 === 7) {
calendarOptions.headerToolbar.right = 'myCustomButton dayGridMonth,timeGridWeek,timeGridDay'
weekViewColumn.value = [{ key: 'ownUserName', width: 100 }]
for (let d = 0; d < 5; d++) {
let day = new Date(view.start.getTime() + (24 * 3600 * 1000 * d))
weekViewColumn.value.push({
key: date2Str(day),
title: `${weekDay[d]}/${day.getMonth() + 1}-${day.getDate()}`
})
}
// weekViewData.value
} else {
calendarOptions.headerToolbar.right = 'dayGridMonth,timeGridWeek,timeGridDay'
}
myMatchList(timeObj)
// 记录视图
submitView({ view: view.view.type })
},
4.周看板
周看板为单独封装在FullCalendar上的,非FullCalendar原生自带功能。
主要需求是为了帮助领导助理及时安排、记录、处理领导的日程,方便规划领导整体行程。
效果图
使用FullCalendar中的 customButtons配置项,添加看板按钮
customButtons: {
myCustomButton: {
text: '看板',
click: function() {
isWeekViewShow.value = true
}
}
},
看板列表与订阅人列表相同,在获取到事件数据后,对事件进行处理
// 处理周视图弹窗数据--以 ownUser 维度组合
if (item.ownUser) {
let eIdx = weekViewData.value.findIndex(el => el.ownUser == item.ownUser)
if (eIdx === -1) {
weekViewData.value.push({
ownUser: item.ownUser,
ownUserName: item.ownUserName,
[item.startTime.slice(0, 10)]: [{
subject: item.subject,
id: item.id
}]
})
} else {
if (Object.hasOwn(weekViewData.value[eIdx], item.startTime.slice(0, 10))) {
weekViewData.value[eIdx][item.startTime.slice(0, 10)].push({
subject: item.subject,
id: item.id
})
} else {
weekViewData.value[eIdx][item.startTime.slice(0, 10)] = [{
subject: item.subject,
id: item.id
}]
}
}
// 处理跨天日程
if (calcDays(item.startTime, item.endTime) > 0) {
for (let n = 1; n <= calcDays(item.startTime, item.endTime); n++) {
weekViewData.value[eIdx][getNextDay(item.startTime, n)] = [{
subject: item.subject,
id: item.id
}]
}
}
}
在切换视图时处理相关数据
// 周视图-领导视图交互及数据处理
if ((view.end.getTime() - view.start.getTime()) / 1000 / 3600 / 24 === 7) {
calendarOptions.headerToolbar.right = 'myCustomButton dayGridMonth,timeGridWeek,timeGridDay'
weekViewColumn.value = [{ key: 'ownUserName', width: 100 }]
for (let d = 0; d < 5; d++) {
let day = new Date(view.start.getTime() + (24 * 3600 * 1000 * d))
weekViewColumn.value.push({
key: date2Str(day),
title: `${weekDay[d]}/${day.getMonth() + 1}-${day.getDate()}`
})
}
静态结构
<el-dialog v-model="isWeekViewShow" width="60%" class="week-view-dialog">
<template #header>
<h1>看板</h1>
</template>
<div class="dialog-content">
<el-table :data="weekViewData" border stripe show-overflow-tooltip height="680">
<el-table-column v-for="col in weekViewColumn" :prop="col.key" :key="col.key" :label="col.title" :width="col.width">
<template #default="scope">
<div style="min-height: 63px;" :class="col.key == 'ownUserName' ? 'flex items-center' : ''">
<p v-if="col.key == 'ownUserName'" style="">{{ scope.row[col.key] }}</p>
<ul v-else>
<li v-for="(li, idx) in scope.row[col.key]" class="text-ellipsis whitespace-nowrap overflow-hidden">
<el-tooltip :content="li.subject" placement="top" effect="light">
{{ `${idx + 1}.${li.subject}` }}
</el-tooltip>
</li>
</ul>
</div>
</template>
</el-table-column>
</el-table>
</div>
</el-dialog>
四、总结
以上就是基于Vue3 + FullCalendar实现会议日程预约管理系统开发方案,在本方案中,我们对el-calendar的二次开发,订阅功能的实现,FullCalendar的技术说明等等...内容过多,所以有些地方并没有详细说明,若您有什么疑问或对我的内容进行指正,欢迎您在下方进行评论探讨。
希望本篇内容对您有所帮助。
转载自:https://juejin.cn/post/7361943620931420212