ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Vue 3项目集成Fullcalendar完整指南:从基础渲染到日程增删改查实战

Vue 3项目集成Fullcalendar完整指南:从基础渲染到日程增删改查实战 简介本资源是一套基于Vue.js开发日程管理系统的完整项目示例面向前端开发者及Vue初学者解决在Vue应用中集成FullCalendar实现事件展示、拖拽交互、多视图切换等核心日程功能的实践难题。压缩包共102个文件包含4个核心Vue组件文件含日历主组件与事件管理模块、3个关键JS逻辑脚本涵盖API模拟、事件增删改及插件配置、11个Sample示例用于快速验证不同视图如日/周/月网格与时间轴以及HTML、CSS、JSON配置等支撑文件整体体积仅1.32MB轻量易上手。已有1577人学习下载项目结构清晰、注释详尽提供从Vue CLI初始化、依赖安装、插件注册、数据绑定到事件监听的全流程可运行代码并内置模拟数据与常见交互逻辑如eventClick、dragdrop响应开箱即用便于二次开发与教学参考。 做日程安排功能的时候我在Vue项目里对比过好几个日历方案最后选了Fullcalendar。这中间其实绕了不少弯网上关于Fullcalendar的教程大多停留在安装完、能显示日历的层面真正把日程的增删改查、视图切换、拖拽交互全部跑通并且处理好各种边界情况的太少了。这篇就是我从零开始接入、踩坑、完善的全过程记录希望能帮你直接把这块功能做得更完整。适合谁看正在用Vue不管是Vue2还是Vue3做后台管理、日程管理、预约系统、排期系统需要日历视图展示并操作日程的开发者。我会把核心概念、完整代码、会遇到的问题一次性讲透。1. 为什么选Fullcalendar日程需求远不止显示日历先说我为什么会选这个库而不是自己写或者用别的组件库自带的日历。当时的需求是这样的需要月视图、周视图、日视图三种模式切换日程可以点击查看详情、拖拽改时间、点击日期快速创建还要支持一天内多个时段事件。刚开始我也想过直接用Element Plus的日历组件但它的定位更像日期面板没有周/日时间轴事件渲染能力非常弱antd的Calendar也类似做做打卡、展示节假日还行做真刀真枪的时间管理不够用。自己写的话更不现实。月视图、周视图、日视图的日期算法、跨月渲染、时间轴网格、拖拽换时间、移动端适配这些别说从零写就是抄一套都要按周计算工作量。Fullcalendar的定位非常精准——它就是专门做可拖拽、可编辑、多视图的日程日历库市面上很多成熟SaaS产品的时间管理模块底层就是它。我在选型时候看中的核心能力有四个视图体系完整dayGridMonth月历、timeGridWeek周时间轴、timeGridDay日时间轴、listWeek列表视图开箱即用。事件Event系统健壮支持本地数组、JSON接口、函数式拉取三种事件来源事件的增删改查都有对应回调。交互能力强大接上interaction插件后支持拖拽改期、拉伸改时长、点击日期创建日程这些在原生日历里是最费劲的部分。框架适配成熟官方维护fullcalendar/vue3和fullcalendar/vue2两个适配包不是社区第三方封装出问题能查到官方文档。这里要注意它不是一个集成了配置界面的业务组件它是一个日历内核。UI风格、弹窗、表单、颜色这些要靠你自己搭配。但这也正是我推荐它的原因——你拿到的是没有绑定UI框架的日历内核可以自由定制成和项目风格一致的样子和Element Plus或者Ant Design都不冲突。2. 依赖安装与版本匹配最容易翻车的一步2.1 安装命令和包之间的关系Fullcalendar从v5开始采用了插件化的架构。核心包只提供日历渲染和事件管理的基础能力视图、交互、列表这些功能全部做成了独立插件包你需要什么就装什么。这样做的目的是控制打包体积但也意味着刚上手的人经常漏装插件导致某些功能点了没反应。直接给命令Vue3项目npm install fullcalendar/vue3 fullcalendar/core fullcalendar/daygrid fullcalendar/timegrid fullcalendar/interaction fullcalendar/list如果是Vue2项目npm install fullcalendar/vue2 fullcalendar/core fullcalendar/daygrid fullcalendar/timegrid fullcalendar/interaction fullcalendar/list这几个包各是什么角色我用表格捋清楚包名作用是不是必须fullcalendar/vue3或vue2Vue桥接层把日历组件包装成Vue组件必须fullcalendar/coreFullcalendar核心负责渲染和调度必须fullcalendar/daygrid月视图、日网格视图必须fullcalendar/timegrid周视图、日视图的时间轴需要周/日视图就装fullcalendar/interaction点击日期、拖拽事件、改变事件时长等交互需要交互就装fullcalendar/list列表视图可选官方文档里还有一个fullcalendar/multimonth包是多月四联/半年视图一般项目用不上我没装。2.2 版本匹配的几个关键点这一块是大家踩坑最密集的地方我按照实际踩过的顺序分别说。第一vue桥接包和Vue主版本必须严格对应。网上很多教程是Vue2时代写的你如果抄过来发现在控制台报Failed to resolve component或者页面直接白屏先检查是不是把fullcalendar/vue2装进了Vue3项目。Vue3必须用fullcalendar/vue3。npm默认会拉取当前能匹配的最新版但如果你项目里有老依赖擦出兼容性冲突npm有时会降级到旧版本所以就出现了同一个包名昨天还好好的今天重新clone代码就报错的怪事。我的建议是装完之后立刻看package.json锁定版本号别让^符号在下次install时给你来一个意外惊喜。第二plugins数组不能省略。很多人初始化日历的时候只写了initialView和events结果发现点击日期没反应、拖拽功能是灰的找半天原因其实是interaction插件没在plugins数组里注册。Fullcalendar的插件跟Vue的插件注册逻辑类似你在模板里用哪个能力就得在plugins数组里带上对应插件。后面代码示例里我会把这个数组写全。第三如果你用了Vite做构建工具记得确认依赖是预构建状态。Vite首次启动时会自动预构建node_modules里的CommonJS依赖Fullcalendar官方包大部分是ESM正常情况没问题但我遇到过node_modules里的缓存元数据过期导致模板解析异常清一下node_modules/.vite缓存重启就好。2.3 全局样式引入在Vue组件中引入Fullcalendar还需要把核心样式和各插件的样式单独引进来。如果你用的是打包工具直接在组件script区域引入import fullcalendar/core/vdom // vue3版本需要 import fullcalendar/daygrid/index.css import fullcalendar/timegrid/index.css import fullcalendar/list/index.css这段很容易被漏掉。漏了之后日历能渲染但所有视图切换按钮、日期格子、事件块都是错位的没有样式根本没法用。我一开始漏了timegrid的css结果周视图、日视图整个只有一条条横线事件块全挤成一团排查了快一个小时才发现是样式缺失。注意vue3版本的fullcalendar/vue3包里vdom这一行是必须写在core导入之前的官方在升级日志里明确说过跨框架的虚拟DOM调度依赖它。顺序写反的话编译不报错但运行时控制台会出现警告日历内部事件无法正常绑定。3. 核心概念拆解事件、视图、交互是怎么串起来的3.1 事件Event机制是整个日历的灵魂Fullcalendar里日程的官方叫法是Event事件。它不是普通组件的数据对象而是日历内部维护的事件模型。我们可以把它理解成一组约定规则的JSON结构一条最简单的日程长这样{ id: 1, title: 产品评审会, start: 2025-03-18T10:00:00, end: 2025-03-18T11:30:00, allDay: false, backgroundColor: #409eff }字段不多但每个都很有讲究id事件唯一标识增删改查全靠它。不传id后面要做更新的时候会非常难受。title显示在日历格子上的文字。start / end开始和结束时间。格式可以是2025-03-18全天事件也可以是带T的ISO字符串具体时刻事件。如果end不传默认按持续一小时处理具体时长由defaultTimedEventDuration决定默认是01:00:00。allDay是否是全天事件。全天事件在月视图里会置顶显示在周视图/日视图里显示在日期头部区域。backgroundColor / borderColor / textColor事件的颜色。你可以把Event对象理解为日历的核心数据单元后面所有操作——点击、拖拽、编辑、删除——最终都是围绕一个Event对象展开的。3.2 三种视图模式的工作逻辑Fullcalendar的视图类型非常多但项目里最常见的就三个dayGridMonth月视图主历团队会用这种。它按月展示日期格子全天事件显示在格子上方timed事件显示在下边。timeGridWeek周视图做排班、预约系统最常用。一周七天按列排开纵轴是时间默认从00:00到24:00事件按时间坐标放在对应的位置。timeGridDay日视图单日时间轴适合做当天的详细安排。这三个视图的数据来源都是同一个events配置也就是说你只需要维护一份事件数据源Fullcalendar内部会按照当前视图自动计算哪些事件该显示、显示在什么位置。明白这一点对后面排查为什么事件在月视图能看到切到周视图就没了很有帮助——八成不是数据丢了而是事件的时间范围不在当前周的范围内。3.3 交互插件到底在干什么interaction插件在初始化配置里体现为两个东西一是日历的selectable、editable开关二是回调函数。它其实是底层依赖的另一套模型比如你拖拽一个事件本质上是这个插件在监听鼠标事件然后修改事件对象里的start和end最后触发eventDrop回调把修改过的event对象交给你。如果你没有在回调里把修改结果同步到数据库那这个修改只是看起来变了刷新页面之后就会恢复原样。后面讲到日程CRUD的时候会详细说怎么把这个过程串起来。4. Vue3组件接入实操从静态渲染到跑通第一个版本4.1 最小可用代码我这里用一个Vue3的setup写法把日历完整注册进组件里。template div classcalendar-container FullCalendar :optionscalendarOptions refcalendarRef / /div /template script setup import { ref } from vue import FullCalendar from fullcalendar/vue3 import dayGridPlugin from fullcalendar/daygrid import timeGridPlugin from fullcalendar/timegrid import interactionPlugin from fullcalendar/interaction import listPlugin from fullcalendar/list // 样式必须引入 import fullcalendar/core/vdom import fullcalendar/daygrid/index.css import fullcalendar/timegrid/index.css import fullcalendar/list/index.css const calendarRef ref(null) const calendarOptions { plugins: [dayGridPlugin, timeGridPlugin, interactionPlugin, listPlugin], initialView: dayGridMonth, headerToolbar: { left: prev,next today, center: title, right: dayGridMonth,timeGridWeek,timeGridDay }, locale: zh-cn, buttonText: { today: 今天, month: 月, week: 周, day: 日 }, height: auto, // 事件数据 events: [ { id: 1, title: 产品评审会, start: 2025-03-18T10:00:00, end: 2025-03-18T11:30:00, backgroundColor: #409eff }, { id: 2, title: 发布上线, start: 2025-03-20T09:00:00, allDay: true, backgroundColor: #67c23a } ], // 启用可编辑 editable: true, selectable: true, // 点击日期 dateClick: function(info) { console.log(clicked date:, info.dateStr) }, // 点击事件 eventClick: function(info) { console.log(clicked event:, info.event) }, // 拖拽后 eventDrop: function(info) { console.log(event dropped:, info.event.startStr) } } /script style scoped .calendar-container { padding: 20px; background: #fff; border-radius: 8px; } /style这段已经可以跑起来你会看到月视图正常显示两条事件点日期会打印日期字符串拖拽事件到另一个日期会打印新的开始时间。但这只是第一步。4.2 用动态数据替换静态写死的事件实际项目里事件肯定是从后端接口拉取的。events配置可以接收三种形式数组直接给Event对象数组。JSON字符串日历内部帮你parse不推荐还不如直接给对象数组。函数接收info包含当前视图的起止时间、success回调、failure回调日历切换视图时会自动调用这个函数拉取数据。第三种最实用因为它天然解决了一个问题日历数据量大的时候不可能一次性把所有事件都塞给前端而是根据当前视图范围按需加载。函数式事件源是官方推荐做法const calendarOptions { // ...其他配置 events: function(info, successCallback, failureCallback) { // info.startStr是当前视图的开始时间info.endStr是视图结束时间 // 传这两个参数给后端做时间范围过滤 fetch(/api/events?start${info.startStr}end${info.endStr}) .then(res res.json()) .then(data { // data是后端返回的数组需要保证字段名和Event对象一致 successCallback(data) }) .catch(err { // 失败回调日历会显示loading状态并提示错误 failureCallback(err) }) } }这个函数式事件源的好处是你切换月视图到周视图、点上一页下一页、点击今天按钮回到当前时间日历都会触发它重新拉取对应时间范围内的数据。不需要手动刷新也是Vue响应式之外的一条自动数据流。4.3 关于响应式的坑很多教程里会把events放在ref里然后试图通过修改ref来实时更新日历。能行但有些情况不生效。比如你用splice删掉一个事件或者直接给数组里某个对象的title改了值Vue的响应式能感知到数组元素变化但是Fullcalendar内部的事件缓存并不会自动同步页面视图往往还是旧的。最稳妥的做法是想清楚数据从哪来如果是表单提交后新增日程直接用日历实例的addEvent方法或者重新拉取当前时间段数据。如果是弹窗编辑保存后用getEventById找到日历内部的事件对象调用event.setProp或setDates方法更新或者直接移除旧事件再addEvent。如果是直接改events数组确保整个数组是重新赋值的等于说你把事件源整个换掉了然后日历会重新渲染。实际项目里我的习惯是封装一个refreshEvents方法它在每次增删改查成功之后调用重新把当前视图时间段的事件拉取一遍。省心不容易出状态不同步的bugfunction refreshEvents() { const calendarApi calendarRef.value.getApi() calendarApi.refetchEvents() }refetchEvents会触发events函数重新执行是官方提供的标准刷新方式。5. 日程增删改查的完整链路对接后端接口的实战设计5.1 新增日程dateClick里的完整逻辑点击日历上的空白日期创建日程这是预约类系统最常见的交互。我把完整流程走一遍const handleDateClick function(info) { // 弹出自定义新增表单这里是示例用的modal状态变量 isEdit.value false formData.value { title: , start: info.dateStr, end: info.dateStr, allDay: info.allDay } showForm.value true }dateClick回调的info对象里我最常用的是这三个字段info.dateStr点击日期的字符串格式是YYYY-MM-DD如果是timeGrid视图则带时间。info.allDay是否点击了全天区域月视图里点击日期格子allDay是truetimeGrid周视图点击具体时间段allDay是false。info.jsEvent原生的鼠标事件可以用来做右键菜单等。表单确定之后调用新增接口把数据存到数据库然后拿到后端返回的完整事件对象用addEvent把它加到日历里const confirmAdd async function() { const res await apiCreateEvent(formData.value) if (res.code 0) { const calendarApi calendarRef.value.getApi() calendarApi.addEvent({ id: res.data.id, title: res.data.title, start: res.data.start, end: res.data.end, allDay: res.data.allDay, backgroundColor: res.data.color }) showForm.value false } }为什么新增不干脆刷新整个事件源而是用addEvent因为体验好。如果你只是加了一个新日程就把当前月份所有日程重新从后端拉一遍视觉上日历会闪一下还有可能出现loading空白期。addEvent可以增量地插入一条事件日历会平滑地把它渲染出来。这里有一个细节addEvent传入的对象必须包含id。后端通常用自增ID或者雪花ID我们存下来后面更新、删除都要靠它定位。5.2 编辑与删除eventClick弹出表单点击已有事件弹出编辑详情是最常规的需求。注意eventClick回调里的info.event是日历内部事件对象不是我们后端返回的原始数据它的很多字段是Fullcalendar自己包装出来的。要拿我们自己的业务字段比如事件类型、负责人、备注有两种办法第一种在事件挂载的时候直接用extraField字段塞进去。Fullcalendar官方支持在Event对象上挂自定义字段你在构建事件数据时给对象加一个属性比如calendarApi.addEvent({ id: res.data.id, title: res.data.title, start: res.data.start, end: res.data.end, allDay: res.data.allDay, extendedProps: { type: res.data.type, owner: res.data.owner } })用extendedProps包裹自定义字段是官方推荐的方式Fullcalendar会把它原样保留访问的时候通过event.extendedProps.type获取。第二种更直接在eventClick回调里拿event.id去后端接口查详情反正编辑本来就要拉取最新数据。删除更直接。拿到event.id后调删除接口成功之后调用日历的removeEventById删除视图上的事件const confirmDelete async function() { const res await apiDeleteEvent(eventId.value) if (res.code 0) { const calendarApi calendarRef.value.getApi() calendarApi.removeEventById(eventId.value) showForm.value false } }这里有个小技巧删除事件后最好执行一次refetchEvents因为某些情况下你在视图上删除的只是当前视图范围的事件但如果事件是跨天的start在前一天、end在后一天月视图上它可能同时渲染在两个日期格里removeEventById一般能一起处理掉但为了保险起见我每次删除后都会刷新一遍。5.3 拖拽改期与拉伸改时长eventDrop和eventResize这是日历交互最出彩的功能也是和后端联调最容易出问题的地方。先看代码const handleEventDrop async function(info) { const event info.event // 如果拖拽后时间没变直接return if (info.oldEvent.startStr event.startStr info.oldEvent.endStr event.endStr) { return } const res await apiUpdateEvent({ id: event.id, start: event.startStr, end: event.endStr, allDay: event.allDay }) if (res.code ! 0) { // 接口失败要回滚 info.revert() } } const handleEventResize async function(info) { // 跟eventDrop逻辑几乎一样只是resize只改了end const event info.event const res await apiUpdateEvent({ id: event.id, start: event.startStr, end: event.endStr }) if (res.code ! 0) { info.revert() } }这里必须强调info.revert()。当你拖拽一个事件视图上的事件位置会立刻被改变这个改变发生在接口请求之前相当于预提交。一旦接口报错比如网络超时、服务端校验失败你必须调用revert方法来把事件恢复到拖拽前的位置否则用户看到的界面状态会和数据库不一致。这个回滚机制虽然简单但不熟悉的人很容易漏掉我在联调阶段就亲眼见过同事因为没写revert接口失败后日历还停留在拖拽后的位置页面上形成了假数据。另外一个要注意的是如果拖拽时发现后端返回的数据结构跟我们预期的不一致比如后端把start字段强制格式化成YYYY-MM-DD HH:mm:ss而Fullcalendar期望的是ISO字符串你需要在传给apiUpdateEvent之前做一次格式转换。后端接口的字段习惯直接沿用前端实际发送的值我在接口设计阶段就和后端约定好所有日期时间统一使用ISO 8601格式避免前后端来回转格式的麻烦。5.4 时间范围约束给拖拽加条件很多业务场景不允许把事件拖到过去或者不允许拖出工作时间。Fullcalendar提供了eventDragStart、eventDrop等多个钩子但更常用的是eventAllow这个配置它在每次拖拽/调整尺寸前执行返回false就会阻止这次操作const calendarOptions { eventAllow: function(dropInfo, draggedEvent) { // 禁止拖到昨天及更早 const today new Date() today.setHours(0, 0, 0, 0) if (dropInfo.start today) { return false } return true } }还有slotMinTime/slotMaxTime可以限定时间轴的显示范围。比如排班系统只显示早8点到晚22点slotMinTime: 08:00:00, slotMaxTime: 22:00:00,事件可以被拖拽到时间轴范围之外吗默认不行因为timeGrid视图的拖拽范围就是你在slot中看到的时间区间。如果拖到22点之后日历会自动吸附到22:00:00。6. 我实际遇到过的疑难杂症与排查过程6.1 弹窗被日历挡住事件点击后没法编辑这是刚接入Fullcalendar时最头疼的问题。我在组件里用Element Plus的el-dialog做编辑弹窗当点击日历事件时弹窗是弹出来了但被日历内部生成的一些浮层盖在下面整个表单操作不了鼠标移上去全是文本光标。排查过程是这样的起先以为是z-index设置不到位我把el-dialog的z-index调到9999依然被盖住。后来用浏览器开发者工具检查元素发现日历内部用来做拖拽预览的层、点击日期高亮层它们的z-index是动态算出来的而且我这个日历容器刚好设置了transform属性可能是做动画时加的触发了新的层叠上下文把Dialog的z-index压住了。解决方案有两个方向。一是调整日历容器样式不要用transform、filter等会创建层叠上下文的属性二是给弹窗组件加append-to-body让它脱离当前容器挂载到body上。我用的是第二种同时给日历事件的操作按钮设置更高的z-index问题就解决了。6.2 时区导致日期偏移一天事件数据在后端存的是2025-03-18T00:00:00前端显示出来却是3月17号或者反过来总是少一天。这个问题的本质是时区被转换了。Fullcalendar默认使用浏览器的本地时区解析日期字符串如果后端返回的字符串不带时区会被当成服务器本地时间解析但你访问的浏览器可能在另一个时区解析出来的本地时间就偏移了。我的解决办法很粗暴后端在返回所有时间字段时统一带上时区标记比如2025-03-18T00:00:0008:00这样前端解析时就不会再按本地时区猜测。或者更简单前端拿到后端时间后统一做一次格式化再传给日历自己控制解析逻辑避免Fullcalendar内部歧义解析。6.3 弹窗打开后日历宽度错乱还有一次是点击新增日程按钮打开弹窗后日历的宽度瞬间被压扁格子里的事件全堆在一起。这是因为弹窗打开时页面布局宽度变化日历在初始化时计算的尺寸没有重新适配。Fullcalendar提供了一个updateSize方法专门用来解决容器尺寸变化的问题。我在弹窗打开/关闭之后用nextTick调用一下import { nextTick } from vue const refreshCalendarSize async function() { await nextTick() calendarRef.value.getApi().updateSize() }如果你用了Vue的keep-alive或者tab切换也要注意页面从隐藏状态切回来时日历容器宽度可能是0这时候日历还没渲染或者宽度计算错误。在activated钩子或者切换事件的回调里也调用一次updateSize能解决99%的尺寸错乱问题。6.4 组件卸载时事件监听器泄漏Fullcalendar作为第三方库内部会绑定很多DOM事件监听器。Vue的组件卸载机制会销毁组件对应DOM节点但Fullcalendar内部的一些全局监听器比如mousemove、mouseup不会自动清理所以在组件卸载前最好手动销毁日历实例。我在项目的beforeUnmount钩子里做了清理onBeforeUnmount(() { const calendarApi calendarRef.value?.getApi() if (calendarApi) { calendarApi.destroy() } })这个操作看起来多余但如果你在同一个页面上反复切换路由日历实例创建销毁多次能明显感觉到卡顿因为浏览器里积压了很多无效的拖拽监听器。我一开始没写destroy在后台管理系统的列表页和详情页之间来回切换十几趟之后页面开始变得迟钝加上销毁逻辑后才恢复正常。6.5 Vue的key复用导致日历内部状态残留如果你在同一个页面里根据某个变量动态切换显示几个不同的日历比如按项目维度展示多个日历Vue可能会复用缓存的DOM节点Fullcalendar内部维护的state可能出现串台。这个问题排查起来最隐蔽因为页面不报错、数据看起来也对但视图就是不对。解决办法是用key强制Vue在切换时重新创建组件FullCalendar :keyprojectId :optionscalendarOptions /projectId一变整个日历组件重新走一遍挂载流程内部状态完全重置问题就消失了。代价是重新初始化会稍微慢一点点但在业务体量下完全可以接受。6.6 移动端触摸拖拽没有失效最后说一个容易被忽略的地方如果你做了响应式适配希望移动端也能拖拽事件光是安装interaction插件还不够。真机调试时touchstart等触摸事件默认是150ms后才触发click拖拽手感会非常差感觉像卡住了。Fullcalendar官方提供了一个配套的pointer事件库但Vue集成时有时候没有及时引入。我的处理方式是在移动端页面里用CSS touch-action禁掉默认手势并给日历容器设置touch-action: none这样拖拽响应会更跟手。如果你发现移动端拖拽完全失效优先检查是否被某个全局的touch事件处理层拦截了。7. 视觉定制与体验优化不要止步于能用当功能链路跑通之后剩下的就是把日历做得更像产品而不是一个开发Demo。我分享几个实际改成型的优化点。7.1 自定义事件块的样式默认情况下事件块就是一整条色带。如果你的日程有很多类型比如待办、会议、出差、请假最好按类型区分颜色。事件对象里的backgroundColor / borderColor支持你动态指定也可以在初始化配置里用eventDidMount回调针对每个事件做额外的DOM操作eventDidMount: function(info) { const el info.el if (info.event.extendedProps.type meeting) { el.style.borderLeft 4px solid #f56c6c el.style.fontWeight bold } // 给事件加一个状态角标 if (info.event.extendedProps.status overdue) { const badge document.createElement(span) badge.textContent 逾期 badge.style.cssText position:absolute;right:4px;top:2px;color:#f56c6c el.appendChild(badge) } }eventDidMount相当于每个事件渲染完成后的生命周期钩子你可以在里面做任何DOM操作动态加角标、改样式、绑定自己的事件监听都可以。7.2 自定义视图切换按钮headerToolbar提供了左右切换和视图按钮的组合但如果你觉得不够可以用自定义按钮。在headerToolbar里加一个dayGridMonth、timeGridWeek、timeGridDay之外的按钮比如打印或者导出headerToolbar: { left: prev,next today, center: title, right: dayGridMonth,timeGridWeek,timeGridDay customButton }, customButtons: { customButton: { text: 导出, click: function() { // 导出当前视图的数据 handleExport() } } }这样导出的场景就很好实现了。我在实际项目里做了个导出周报按钮点击时调用列表视图的数据导出成Excel整个功能非常顺畅。7.3 大列表数据的懒加载优化当你的事件数量非常大比如一个会议室一个月有上千条预约把所有事件一次性加载会让月视图的渲染变得卡顿。解决方案是用函数式事件源配合日历自身的按需拉取机制。Fullcalendar在每次视图切换时都会传给你当前视图的start/end你传给后端后端只返回这个时间段内的事件。这样无论是月视图还是年视图如果配置了多列显示前端每次最多渲染几百条事件体验会很流畅。我做了个3000条事件模拟数据测试一次性塞进events数组月视图首屏渲染要2秒多切成函数的按需加载后每次渲染在100ms以内。差距非常明显。7.4 空状态与加载提示如果你的日程系统在某个时间段内没有事件日历默认是一片空白用户可能会以为系统坏了。我给日历加了一个空状态提示loading: function(isLoading) { if (isLoading) { // 显示loading我用的ElMessage或者加一个mask } else { // 隐藏loading } }这个loading回调在events函数执行前后会被触发如果在events里拉取数据比较慢用户可以明显感知到日历正在加载而不是看着空白页面干等。8. 项目封装思考把日历封装成可复用业务组件当你做完一个页面之后会发现如果直接把Fullcalendar塞进业务页面代码会越来越臃肿。事件点击弹窗、拖拽改期提示、视图切换状态管理全部堆在同一个组件里后面维护起来会很头疼。我第二次做类似的日程功能时抽成了三个层基础层一个Calendar.vue通用组件封装Fullcalendar的注册、样式、核心配置对外暴露日期点击、事件点击、拖拽调整等纯事件回调不关心业务。中间层一个ScheduleCalendar.vue业务组件接收后端接口配置内部处理好事件数据的格式转换、接口调用、事件增删改查对外只暴露日程数据。页面层具体的页面负责弹窗表单、权限控制以及其他业务逻辑。这样的分层之后如果你项目里有两三个页面都需要日程功能每个页面只需要写自己特有的表单和接口参数日历交互的部分不用重复开发。我第二次实现的时候只花了第一次大概五分之一的时间而且样式和交互保持完全一致。封装的时候有一个点需要提前想清楚Fullcalendar的options是配置对象官方推荐用setup方式传如果你直接往props里塞对象有的版本在数据更新时不会自动应用。更好的方式是把options作为一个响应式对象放在setup里让日历组件自然感知到它的变化。9. 一段完整的业务级示例代码最后给一段我在实际项目里收敛过版本的可运行代码整合了上面所有关键点。这是一个Vue3的ScheduleCalendar组件它接收日程数据支持拖拽、点击、新增和删除并且所有操作都会触发事件通知父组件由父组件处理后端接口。template div classshedule-wrapper FullCalendar refcalendarRef :optionscalendarOptions :keyreloadKey / /div /template script setup import { ref, computed } from vue import FullCalendar from fullcalendar/vue3 import dayGridPlugin from fullcalendar/daygrid import timeGridPlugin from fullcalendar/timegrid import interactionPlugin from fullcalendar/interaction import listPlugin from fullcalendar/list import fullcalendar/core/vdom import fullcalendar/daygrid/index.css import fullcalendar/timegrid/index.css import fullcalendar/list/index.css const props defineProps({ events: { type: Array, default: () [] } }) const emit defineEmits([ dateClick, eventClick, eventDrop ]) const calendarRef ref(null) const reloadKey ref(0) const calendarOptions computed(() ({ plugins: [dayGridPlugin, timeGridPlugin, interactionPlugin, listPlugin], initialView: dayGridMonth, headerToolbar: { left: prev,next today, center: title, right: dayGridMonth,timeGridWeek,timeGridDay }, locale: zh-cn, buttonText: { today: 今天, month: 月, week: 周, day: 日 }, height: auto, events: props.events, editable: true, selectable: true, eventAllow: (dropInfo) { // 禁止拖到过去示例 const today new Date() today.setHours(0, 0, 0, 0) return dropInfo.start today }, dateClick: (info) { emit(dateClick, info) }, eventClick: (info) { emit(eventClick, info) }, eventDrop: (info) { emit(eventDrop, info) }, eventResize: (info) { emit(eventDrop, info) } })) function getApi() { return calendarRef.value?.getApi() } function refresh() { getApi()?.refetchEvents() } function destroy() { getApi()?.destroy() } defineExpose({ getApi, refresh, destroy }) /script style scoped .shedule-wrapper { width: 100%; height: 100%; } /style父组件使用的时候template ScheduleCalendar :eventseventList dateClickhandleDateClick eventClickhandleEventClick eventDrophandleEventDrop / /template script setup import { ref } from vue import ScheduleCalendar from /components/ScheduleCalendar.vue import { apiGetEvents, apiUpdateEvent } from /api/schedule const eventList ref([]) const loadEvents async () { const res await apiGetEvents({}) eventList.value res.data } const handleDateClick async (info) { // 打开新增弹窗将info.dateStr填入表单的start字段 console.log(新增日程开始时间, info.dateStr) } const handleEventClick async (info) { // 打开编辑弹窗回显info.event的信息 console.log(编辑日程, info.event.id) } const handleEventDrop async (info) { const res await apiUpdateEvent({ id: info.event.id, start: info.event.startStr, end: info.event.endStr }) if (res.code ! 0) { info.revert() } } loadEvents() /script父组件负责所有接口调用子组件只负责表现和交互。这套结构我用下来无论是快速开发还是后续维护都挺顺手。如果你项目里已经有Element Plus就继续用完全不冲突如果你用的是Antd Vue也没任何问题Fullcalendar不绑定组件库。做完这些之后我再回头看当初要不要自己写日历的选择庆幸没有冲动。Fullcalendar虽然上手的时候文档有点绕、插件机制要适应但一旦把它的事件模型这套逻辑搞清楚它在日程管理这块能帮你省下大量的开发时间而且稳定性远比自己用div拼靠谱。如果你在接入过程中遇到我上面没写到的问题欢迎在评论区把你的现象和错误日志贴出来我们一起排查。本文还有配套的精品资源点击获取
返回列表