简介:FullCalendar 1.5.3 是一套用于在网页中构建日程、事件和时间表的 JavaScript 组件包,面向需要快速实现日历管理功能的前端开发者和 Web 项目团队。它支持日、周、月及列表视图,可加载 JSON、PHP 等动态数据源,并提供多语言配置,方便与常见后端系统结合。整个压缩包共 35 个文件,以 JS、CSS、HTML 为主,同时包含 PNG 图标、PHP 示例、文本说明及数据库文件,整体约 159KB,轻量易部署;未压缩版与压缩版脚本分别适合开发调试和生产引用,配套样式表与打印样式可直接处理常规展示需求。预览显示包内已提供多种视图、外部拖拽、可选中日期和 Google 日历接入等可运行示例,并附有 jQuery 依赖,便于对照 API 完成二次开发。目前已有 175 人学习下载,适合希望低成本接入在线日程展示、事件管理或预约功能的中高级前端开发者参考。 做前端时间管理类功能,日历永远是绕不开的坎。从最早的只在后台管理里放一个简单的月视图,到后来要做周视图、日视图、资源时间线、拖拽修改日程,需求一复杂,自己手写就彻底不现实了。我前前后后换过好几个方案,最后固定在FullCalendar上,一直用到现在。这篇文章不聊官网文档里的Hello World,就聊我在真实项目里怎么选型、怎么接线、怎么填坑,以及那些文档里没写明白但你一定会踩到的细节。
FullCalendar是一个开源的JavaScript日历库,用它可以在React、Vue、Angular甚至原生JS里快速实现支持月视图、周视图、日视图、列表视图的事件日历,并且具备拖拽、缩放、点击编辑等交交互能力。如果你要做的功能是“带数据的日历”,而不是“画一个日历外观”,那它就是现成的轮子。下面这几年的实战内容,希望能帮你省下几个通宵排查问题的时间。
1. 为什么是FullCalendar:项目选型背后的思考
1.1 这个库解决了什么问题
我接手过好几个和日程、排班、课程表相关的项目,最开始的实现方式基本都是自己写一个表格,把一个月按周拆开,再往单元格里塞数据。听着简单,实际做起来全是坑:月份第一天不是周一,每周跨月的时候要怎么补格子,点击上个月按钮之后日期范围怎么算,事件跨越多天的时候DOM要怎么处理。这些逻辑单独拿出来都不难,但合在一起就是一个完整的状态机,写一遍能掉一层头发。
FullCalendar替我把这一整块都解决了。传入一个日期范围,它自己计算视图的起止时间、渲染格子、处理跨月跨周;传入事件数组,它负责把事件按时间投射到正确的位置;用户拖拽后,它抛出事件,我只需要在回调里更新数据就行。这就不只是一个“UI组件”,而是一个完整的日程管理引擎。
1.2 和其他方案的对比
用FullCalendar之前,我认真比较过几条技术路线。
第一条是自己基于表格手写。好处是可控性高,但坏处特别明显:研发周期长、边界场景多、后期维护成本高。做一个只读月视图可能要两三天,做到可拖拽、可跨视图编辑至少要一两周,而且每加一个新交互都要从头推一遍状态逻辑。
第二条是使用其他日历库,比如React Big Calendar。平心而论,它在React项目里很方便,可是配置灵活性、插件生态和文档完整度都不如FullCalendar。比如资源时间线视图(就是那种左侧人员列表、右侧按小时排班的模式),FullCalendar提供了一个Scheduler插件直接搞定,而React Big Calendar需要自己组合组件实现。
第三条就是FullCalendar。它的优势很明显:本身是框架无关的核心库,提供React、Vue、Angular适配器;包体积可控,可以按需引入插件;事件对象模型设计得很完整,覆盖了真实业务里绝大多数需求。我在两个项目里用了之后,觉得它确实能扛住复杂场景,就稳定用它了。
1.3 版本选择的考量
FullCalendar在2022年发布了v6,和v5对比,从上到下来了一次重构。最大的变化是样式系统全套换成了CSS变量,颜色、边框、圆角这些全部可以覆盖,而不是像以前那样去改深层SCSS。这对我这种习惯在业务层统一设计语言的开发者来说非常友好。
v6还调整了包结构,核心功能拆成了@fullcalendar/core加插件的形式,编译层面也全面ESModule化。如果你用的是Vite、Webpack 5这类现代构建工具,tree-shaking能帮你把最终打进项目的代码控制到很小的体积。
我个人建议新项目直接用v6。旧项目如果不涉及大规模重构,留在v5也能跑,但早晚要升级。v6的中文文档资源虽然没有英文全,但核心API和v5基本保持一致,很多旧经验都能直接迁移过来。
2. 核心细节解析与实操要点
2.1 视图体系:日历的四种显示模式
FullCalendar的视图体系,一句话概括:视图决定了你“按什么维度展示时间段”。
dayGridMonth是经典月视图,以自然月为容器列出所有事件,适合做排期总览。timeGridWeek和timeGridDay是按时间轴排列的周视图/日视图,每个事件按起止时间渲染成色块,适合做会议室预订、课程安排。listWeek是以列表形式罗列事件,适合放在手机端或做辅助信息流。
多视图最常见的配置方法是这样:
import FullCalendar from '@fullcalendar/react' import dayGridPlugin from '@fullcalendar/daygrid' import timeGridPlugin from '@fullcalendar/timegrid' import interactionPlugin from '@fullcalendar/interaction' import listPlugin from '@fullcalendar/list' <FullCalendar plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin, listPlugin]} headerToolbar={{ left: 'dayGridMonth,timeGridWeek,timeGridDay,listWeek', center: 'title', right: 'prev,next today' }} initialView="dayGridMonth" />有一点值得注意:headerToolbar里配置的按钮名称,必须和插件的视图ID一一对应。listWeek属于list插件,timeGridDay属于timeGrid插件,只要少引一个,工具栏上对应的按钮就不会出现,而且不报错,这是很多新手容易忽略的点。
2.2 事件源与事件对象:数据从哪里来、长什么样
事件源(EventSource)是FullCalendar里数据接入的统一入口。你可以把它理解成“一个日历里可以挂多个数据来源”,比如同时挂一个团队日程源和一个个人日程源,每个源都可以独立控制是否显示、用什么颜色、从哪里加载。
事件源有三种定义方式:固定数组、JSON地址、函数。固定数组适合静态数据;JSON地址适合后端给好接口直接用;函数形式是我最推荐的,因为它在每次视图切换、刷新时都会被调用,可以带上当前视图的开始时间和结束时间出去请求数据,只拉当前可见范围内的数据。
事件对象的核心字段需要注意:
id:事件的唯一标识,拖拽更新回传的时候靠它定位数据。title:显示在日历上的文本。start/end:起止时间,可以是ISO字符串,也可以是Date对象。end是可选的,不传时FullCalendar会按默认时长处理。allDay:是否全天事件。全天事件在月视图顶部显示,时间格子里不占钟点位置。backgroundColor/borderColor/textColor:覆盖默认外观。extendedProps:自定义字段。这是我最常用的,比如事件关联的数据库ID、人员对象、订单编号,全挂在里面,回调时原样返回。
我实测一个经验:后端返回的数据字段名往往和FullCalendar不一致。比如后端叫startTime,FullCalendar要的是start。这种情况不用改后端,在前端做一次映射就好,保持后端模型稳定,把适配逻辑放在前端。
2.3 交互插件:拖拽、缩放、点击的底层机制
交互能力分散在两个插件里。@fullcalendar/interaction负责基础的点击事件、选中时间段、拖拽移动事件、拖拽调整事件起止时间。@fullcalendar/resource-timeline这类Scheduler插件,是在交互插件之上叠加资源维度的视图能力。
拖拽移动触发的事件是eventDrop,拖拽调整时长触发的是eventResize,它们都是在用户松开鼠标之后才触发,回调参数里有一个event对象和一个delta对象,delta表示从原位置偏移了多少毫秒。业务上要在回调里做的,就是拿到新的事件起止时间,去调后端接口更新,完成后再刷新日历数据。
点击和选中时间段也是高频用法。dateClick可以处理“点击某一个日期格子”的动作,比如弹出新建日程的弹窗;selectable开启后用户可以在视图上拖拽框选一个时间段,松开后触发select回调,非常适合做“在日历上直接创建日程”的交互。
3. 实操过程与核心环节实现
3.1 三步快速搭建一个基础日历
以React项目为例,第一步安装依赖:
npm install @fullcalendar/react @fullcalendar/core @fullcalendar/daygrid @fullcalendar/timegrid @fullcalendar/interaction @fullcalendar/list第二步引入组件和插件,做最小配置:
import FullCalendar from '@fullcalendar/react' import dayGridPlugin from '@fullcalendar/daygrid' import timeGridPlugin from '@fullcalendar/timegrid' import interactionPlugin from '@fullcalendar/interaction' import zhLocale from '@fullcalendar/core/locales/zh-cn' function Calendar() { return ( <FullCalendar plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin]} locale={zhLocale} initialView="dayGridMonth" events={[ { id: '1', title: '产品评审', start: '2025-05-06T10:00:00', end: '2025-05-06T11:30:00' }, { id: '2', title: '开发周会', start: '2025-05-07T14:00:00' } ]} /> ) }到这一步,一个带工具栏、支持中文显示、有月周日视图的基础日历就出来了。整个配置的量级,比手写一个月的格局小太多。
第三步是把日历的宽高撑起来,这算一个比较隐蔽的问题。FullCalendar默认样式是自适应容器宽度的,但高度在大多数情况下需要手动控制。最稳妥的做法是用CSS变量覆盖:
.calendar-wrapper { height: 600px; } .calendar-wrapper .fc { height: 100%; }或者直接在组件上传height属性,比如height={600}或height="100%"。如果不管高度,月视图和列表视图可能正常,但时间视图会出现滚动条不出现、事件错位等奇怪问题。
3.2 把数据接进来:与后端接口对接的常规做法
真实项目里不可能把事件写死在前端,数据几乎都来自后端。我推荐用函数形式的事件源,原因前面说过:每次视图切换、数据刷新时,它都会带着当前视图的开始时间、结束时间去请求接口。
import dayGridPlugin from '@fullcalendar/daygrid' import timeGridPlugin from '@fullcalendar/timegrid' import interactionPlugin from '@fullcalendar/interaction' // 统一的请求封装,fetchEvents 示意 async function fetchEvents(start, end) { const params = new URLSearchParams({ start: start.toISOString(), end: end.toISOString() }) const res = await fetch(`/api/events?${params.toString()}`) const json = await res.json() return json.data.map(item => ({ id: item.id, title: item.name, start: item.beginTime, end: item.endTime, extendedProps: item })) } <FullCalendar plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin]} initialView="dayGridMonth" events={fetchEvents} loading={isLoading => { // isLoading 为 true 时显示 loading,false 时隐藏 }} />这里有两个很关键的细节。第一个是start和end参数的类型,日历默认传的是Date对象。直接传给后端最好先格式化成固定格式,尤其是当后端对时区敏感的时候。第二个是返回的数据必须是数组,而且数组里每一项最好带id,没有id的话,拖拽后Frontend很难精确知道是哪个事件更新了,会导致视图重渲染丢失数据。
接口正好返回空数组时,日历不会报错,只是显示空白。实际情况中,这更多说明后端接口在前端可见范围内没有返回数据,而不是日历配置错误。
3.3 给日历加上拖拽和调整,实现可编辑日程
只读日历只能看,能拖拽才叫真正的日程管理。启用拖拽交互需要加@fullcalendar/interaction插件,并在日历上打开editable。
const handleEventDrop = async (info) => { const { event, oldEvent } = info const id = event.id const newStart = event.start.toISOString() const newEnd = event.end ? event.end.toISOString() : null // 乐观更新:先改前端,再请求后端 try { await updateEvent(id, { start: newStart, end: newEnd }) } catch (e) { // 请求失败,还原 info.revert() } } <FullCalendar editable={true} eventDrop={handleEventDrop} eventResize={handleEventDrop} selectable={true} select={handleSelect} />info.revert()是官方提供的回滚能力,调用之后事件会自动回到拖动前的位置。在实际项目里,我一般把更新请求和前端状态改动的顺序设计成“先改前端数据,再发后端请求,失败就revert”,这样用户体验最好,也不会出现点击后长时间无响应的情况。
再说select回调。用户按住鼠标从周一的9点拖到10点,松开后select事件会返回start、end。可以在回调里弹一个Modal,让用户输入标题再保存。新建之后把新事件refetchEvents()或直接追加到日历数据里,页面会立刻显示。
3.4 中文本地化和其他高频配置
FullCalendar默认是英文,中文切换非常简单,只需要引入中文本地化包:
import zhLocale from '@fullcalendar/core/locales/zh-cn' <FullCalendar locale={zhLocale} />locale会同时影响按钮文字、月份标题、星期显示、日期格式等。如果项目有中英文切换,直接把locale变量换成对应语言包即可。
另一个高频需求是控制可选的视图按钮。业务中经常只让用户看周视图和日视图,不想开放月视图入口:
headerToolbar={{ left: 'title', center: '', right: 'prev,next today timeGridWeek,timeGridDay' }}还可以用initialDate指定日历默认定位到某个日期,用firstDay设置一周从星期几开始(国内一般设1,表示周一),这些细节虽然小,但都直接影响用户对产品的感知。
4. 常见问题与排查技巧实录
4.1 事件的开始时间总是差8个小时
这个问题我遇到太多次了。日历里的时间显示正常,但通过eventDrop回调拿到的event.start时间戳,转到本地展示时发现多了8小时或少8小时。根源几乎都是时区解析问题:后端返回的ISO字符串带有时区偏移,比如2025-05-06T10:00:00Z,FullCalendar会按当地时间解析;而后端返回的是2025-05-06T10:00:00无时区标记的字符串,FullCalendar会默认当成本地时间,转成时间戳时可能就出现了偏移。
实践经验是:协作时全链路统一用ISO 8601格式,后端返回带时区偏移的时间,前端展示时用dayjs或Intl.DateTimeFormat做格式化,不要手动拼字符串。如果你确认后端返回的就是无时区的“墙上时间”,那在传给FullCalendar之前先给末尾补上本地时区偏移,可以规避很多莫名奇妙的偏差。
4.2 事件拖拽之后视图没刷新
有一种情况是eventDrop回调里用了alert或console.log调试,事件位置变了,但后端数据没更新,日历重渲染之后又变回原样。这不是日历的bug,而是数据源没有变化,FullCalendar的渲染基于数据源,不是基于DOM。
所以要先确认事件源是函数还是静态数组。函数形式的事件源默认在每次视图切换时重新执行,但拖拽后不会自动重新请求。需要在更新接口成功后,手动调用calendarRef.current.getApi().refetchEvents()强制重新拉取数据。
我在项目里已经养成了一个习惯:所有数据变更操作(增删改事件)完成后,统一调一次refetchEvents(),保证视图和数据始终同步,绝不依赖组件内部状态去猜。
4.3 月视图事件太多,挤成一团看不清
事件数量一多,月视图默认会显示eventMaxStack: 2,也就是最多垂直叠2条,超出部分折叠成“+N更多”,点击可以展开弹层。这个默认值在很多业务里不够用,可以通过设置eventMaxStack调大,但最大不建议超过5,否则单元格会很高,月视图看起来不像月历,更像垂直列表。
另一个方案是把月视图的事件改成只显示一个小圆点,点击时在旁边的面板里看详情,这是很多SaaS产品在移动端的做法。FullCalendar里可以通过eventDisplay: 'list-item'或自定义eventContent来实现,视觉效果干净很多。
还有一点是别把dayMaxEvents和eventMaxStack搞混。dayMaxEvents控制的是“某一天最多显示多少个事件条目”,eventMaxStack控制的是“垂直堆叠几层”,如果都设置了,以它们共同作用的视觉结果为标准,先调dayMaxEvents通常更容易见效。
4.4 移动端长按拖拽不灵敏
FullCalendar在桌面端的拖拽体验很好,但到了手机端,editable开启后,手指长按事件块再拖动,偶尔会触发浏览器的默认行为,比如选中文字、滚动页面。两个处理方法:第一个是在CSS里禁用日历区域内的文字选中:
.fc * { -webkit-user-select: none; user-select: none; }第二个是在日历组件上开启longPressDelay,比如longPressDelay={200}(单位毫秒),让长按判定时间变长,减少误触。注意longPressDelay属于交互插件的配置项,不传的话默认值在移动端是0,所以明显感觉到触摸就触发,加了延迟会舒服很多。
如果产品业务以移动端为主,我建议不要完全依赖拖拽来修改时间,可以同时在eventClick里弹出一个底部抽屉,里面提供“修改开始时间”“修改结束时间”的明确按钮,这样对触屏用户更友好。
4.5 条件刷新、动态权限与按需加载资源
FullCalendar通过refetchEvents()支持整表刷新,通过getEventSources()可以拿到所有事件源,然后单独调用某个源上的refetch()实现部分刷新。在多团队、多标签页共存的场景,这个能力很有用。
权限控制方面,可以动态切换editable的值。比如用户只有只读权限时设置editable={false},前端不用改逻辑,只改一个属性就能锁住所有交互。
最后说一下Scheduler插件。如果你要做会议室预订、医院排班、多人日程的资源时间线,它解决的正是这块核心需求。它属于License插件,有试用版,商用需要购买授权,这一点在选型时提前评估,不要等到上线前才被合规卡住。
最后再分享一点个人心得
用FullCalendar这几年,最深的体会是:它不是一个“日历组件”,而是一个“日程管理运行时”。你在配置里写的不是UI描述,而是对业务规则的声明。事件源、视图、交互回调、时区处理,这些东西想清楚了,换什么前端框架都能顺滑接上。
如果这个项目是第一次用FullCalendar,我的建议是把v6官方示例完整跑一遍,不要跳过任何交互演示,拖拽、缩放、多事件源、资源时间线,每一项都亲手点一点。然后再从最简单的月视图开始,一步步往里加业务逻辑。别一上来就想着上Scheduler插件做时间线,基础视图的配置和联调经验,会让你在后面踩坑时更快定位问题。
本文还有配套的精品资源,点击获取