基于Vue3 + FullCalendar实现会议日程预约管理系统

目录

最终效果图

一、 FullCalendar插件说明

二、技术梳理 

        1. 左上el-calendar日历部分

        2. 左下订阅部分

        3.  右侧FullCalendar

三、实现方案

        1.自定义el-calender

        2.订阅功能

        3.FullCalendar的使用

        4.周看板

四、总结 


最终效果图

        日:

        周:

        月:

一、 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复选框<input type="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的技术说明等等...内容过多,所以有些地方并没有详细说明,若您有什么疑问或对我的内容进行指正,欢迎您在下方进行评论探讨。

希望本篇内容对您有所帮助。

  • 27
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值