likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 58

最终效果图

日:

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

周:

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

月:

基于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.订阅功能

        功能部分, 主要涉及权限的修改,订阅人的添加与取消,修改颜色,主要为业务逻辑,技术难度相对较低,这里只做展示,不进行展开。

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

       样式部分, 主要涉及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原生自带功能。

        主要需求是为了帮助领导助理及时安排、记录、处理领导的日程,方便规划领导整体行程。

        效果图

基于Vue3 + 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
评论
请登录