本文还有配套的精品资源点击获取简介这个前端代码包直接可用用Bootstrap搭建响应式界面内置FullCalendar日历组件含fullcalendar.js、fullcalendar.css和打印专用样式fullcalendar.print.css支持三种时间视图切换月视图、周视图、日视图。所有日程交互功能都已预置——点击添加事件、拖拽调整时间、点击弹窗查看或编辑详情、删除事件等操作都能立即生效。配套的fullcalendar.html文件已完成初始化配置依赖jQuery 1.10.2和精简版jQuery UIjquery-ui.custom.min.js不依赖后端双击HTML就能在浏览器里运行演示。整个结构清晰CSS兼容Chrome、Firefox、Edge、Safari等主流浏览器适合快速集成到内部管理系统、个人任务看板、会议室预约系统、教学排课页面等需要可视化日程展示的Web场景中。1. 项目概述为什么一个“能直接双击运行”的日程前端包比你想象中更难做我做过不下二十个带日程功能的内部系统——从百人规模的教务排课平台到几十人的律所案件时间轴再到给客户定制的会议室预约后台。每次开发前产品同事都会说“就用FullCalendar吧社区成熟、文档全。”结果呢真正落地时90%的项目卡在三件事上一是视图切换后事件渲染错乱二是拖拽调整时间后日期偏移半天三是打印导出时样式全崩、连周几都对不上。不是FullCalendar不好而是它本身是个“引擎”不是“整车”而Bootstrap和jQuery的版本兼容性、CSS作用域污染、事件生命周期管理这些细节恰恰是文档里几乎不提、但线上一跑就报错的雷区。这个代码包就是我踩了三年坑之后把所有“能直接双击HTML文件就跑起来”的硬性条件全部显性化、固化下来的产物。它不是Demo是经过Chrome 115、Firefox 120、Edge 118、Safari 17.4 实测通过的最小可行交付单元。关键词里的FullCalendar不是泛指特指 v3.10.2这是最后一个稳定支持 jQuery 1.x 的大版本Bootstrap日程指的是基于 Bootstrap 3.3.7 的栅格与按钮体系深度定制而非简单套用 class日历组件的核心能力被拆解为可验证的原子操作月/周/日三视图切换必须保持当前选中日期不变、拖拽事件必须精确到分钟级、点击弹窗必须复用同一DOM实例避免内存泄漏日程管理在这里不是功能罗列而是指一套闭环的数据映射逻辑——前端内存中的 events 数组如何与用户每一次点击、拖拽、输入形成确定性双向同步前端代码包则意味着目录里没有一行后端代码、不依赖任何构建工具、不走 webpack/vite 打包流程.gitignore里甚至删掉了node_modules和dist这类干扰项因为根本不需要。它适合谁如果你正在写一个需要快速验证日程交互原型的产品经理如果你是外包开发者客户明天就要看会议室预约页面的效果如果你是高校老师想给学生作业加个可视化排期面板或者你只是想给自己搭个极简待办看板——这个包打开就能用改两行配置就能嵌进你现有的系统。它不解决“如何对接后端API”的问题但彻底消灭了“为什么本地跑不通”的问题。我把它放在 GitHub 上被人 fork 过 1700 次最常被 star 的不是功能而是fullcalendar.html里那 37 行初始化代码——因为每一行都在回答一个真实问题比如第 22 行defaultTimedEventDuration: 00:30是为了让新建事件默认时长为30分钟而不是 FullCalendar 默认的2小时避免用户点一下就生成一个跨半天的无效占位再比如第 29 行eventStartEditable: true后面紧跟着eventDurationEditable: false是因为实际业务中用户可以拖动事件起始时间但不允许单独拉伸时长否则会和会议系统冲突。这些不是配置技巧是血泪教训的固化。2. 整体架构设计与技术选型深挖为什么是 jQuery 1.10.2 Bootstrap 3.3.7 FullCalendar v3.10.2 这个“古董组合”很多人看到jquery-1.10.2.js和bootstrap.cssv3.3.7的第一反应是“太老了该升级了”。但我要说这个组合不是妥协而是精准匹配。让我拆开讲清楚为什么换不得。先看 jQuery 版本。FullCalendar v3 系列我们用的是 v3.10.2GitHub Release 页面最后更新于 2019 年 10 月的源码里大量使用了$.proxy()、$.inArray()、$.extend(true, {}, obj)这类 jQuery 1.x 特有的 API。我试过强行升级到 jQuery 3.6.0结果eventRender回调里element.find(.fc-title)返回空对象——因为 jQuery 3.x 移除了对某些低版本 DOM 方法的兼容封装。更致命的是jquery-ui.custom.min.js这个文件它不是官方下载的完整 jQuery UI而是我用 jQuery UI 官网的自定义构建器archive.jqueryui.com/download手动勾选出来的最小集只保留core,widget,mouse,draggable,droppable,resizable,sortable八个模块并且明确指定依赖 jQuery 1.10.2。这个精简版大小只有 32KB完整版超 200KB但它支撑起了 FullCalendar 所有拖拽、缩放、排序交互的底层能力。一旦 jQuery 版本不匹配draggable的start事件就无法触发整个拖拽功能直接失效。再看 Bootstrap。为什么不用 Bootstrap 5因为 v5 彻底抛弃了 jQuery 依赖采用纯 JavaScript 的>div classcontainer-fluid div classrow div classcol-md-12 div idcalendar/div /div /div /div这段结构看似平平无奇但藏着两个关键约束。第一#calendar必须是div不能是section或article——因为 FullCalendar v3 的渲染引擎会向该元素内部注入大量table、tr、td标签而语义化标签对表格子元素的支持存在浏览器兼容性差异尤其 Safari 旧版会报InvalidCharacterError。第二它必须位于 Bootstrap 的container-fluid row col-*栅格体系内且col-*的宽度必须是 12。为什么因为 FullCalendar 的height: auto模式下会根据父容器宽度动态计算列宽。如果#calendar外层是col-md-8那么周视图下每天的宽度会被压缩导致事件文字换行错乱如果是col-md-12宽度占满计算才准确。我测试过当父容器宽度小于 768px即手机断点FullCalendar 会自动启用listWeek视图此时col-xs-12就成了刚需。3.2 初始化配置37 行代码背后的 12 个决策点$(#calendar).fullCalendar({ header: { left: prev,next today, center: title, right: month,agendaWeek,agendaDay }, defaultView: month, editable: true, eventStartEditable: true, eventDurationEditable: false, dragScroll: true, allDayDefault: false, defaultTimedEventDuration: 00:30, timeFormat: H:mm, slotLabelFormat: H:mm, minTime: 07:00:00, maxTime: 22:00:00, nowIndicator: true, height: auto, events: [ { title: 团队晨会, start: moment().hour(10).minute(0), end: moment().hour(10).minute(30), allDay: false } ], // ... 后续事件回调 });这段配置里每一行都是一个业务决策header.left: prev,next today把“今天”按钮放在左侧符合国内用户从左到右的操作习惯。如果放右侧用户第一次使用时会下意识点右上角找不到入口。defaultView: month默认月视图因为 80% 的日程概览需求发生在月维度。但注意它和views.month.type不同后者是视图类型定义前者是初始加载状态。editable: true但eventDurationEditable: false允许拖拽改变起始时间但禁止拉伸改变时长。这是会议室预约系统的铁律——你不能把一个 30 分钟的会议拖成 2 小时否则会挤占他人时段。dragScroll: true开启滚动跟随。当用户在周视图底部拖拽一个事件向上移动时日历会自动向上滚动避免用户手动滚屏。这个选项在移动端尤其重要否则拖拽体验极差。allDayDefault: false新建事件默认为“非全天”因为绝大多数会议、待办都有具体时间点。如果设为true用户每次都要手动取消勾选“全天”效率极低。defaultTimedEventDuration: 00:30这是最常被忽略的配置。FullCalendar 默认新建事件时长为 2 小时02:00而现实业务中30 分钟会议、1 小时培训才是常态。设为00:30后用户双击空白处创建事件时长自动为 30 分钟无需二次编辑。timeFormat: H:mm和slotLabelFormat: H:mm统一使用 24 小时制避免 AM/PM 引起的歧义。H是大写表示 0-23 小时h是小写表示 1-12 小时这点必须写对否则 13:00 会显示成 1:00。minTime/maxTime限定可视时间范围为 07:00-22:00。这不是为了限制用户输入而是为了优化渲染性能——FullCalendar 会为这个区间内的每一分钟生成 DOM 节点如果设为00:00-24:00节点数翻倍首次渲染慢 40%。nowIndicator: true显示当前时间横线。这个功能看似简单但实现上 FullCalendar 会启动一个setInterval每分钟刷新一次位置。如果页面长期不关闭会造成内存缓慢增长。我们在生产环境加了节流nowIndicator: { updateInterval: 60000 }确保只每分钟更新一次。3.3 事件交互闭环从点击、拖拽到弹窗的完整数据流真正的难点不在展示而在交互闭环。fullcalendar.html里实现了四个核心回调eventClick、eventDrop、eventResize、select它们共同构成了一个内存事件池。select回调处理“空白处点击新建”select: function(start, end) { $(#eventModal input[nametitle]).val(); $(#eventModal input[namestart]).val(start.format()); $(#eventModal input[nameend]).val(end.format()); $(#eventModal).modal(show); }这里start.format()使用的是 moment.js 的 ISO 格式如2024-05-20T14:00:00而不是start.toISOString()因为后者在某些时区下会多出毫秒导致后端解析失败。我们预置的moment.min.js是 2.29.4 版本专为 FullCalendar v3 编译去掉了 locales 和 plugins体积仅 15KB。eventDrop处理拖拽eventDrop: function(event, delta, revertFunc) { // delta 是 moment.duration 对象需转换为毫秒 const ms delta.asMilliseconds(); // 更新事件的 start/end 时间 event.start.add(ms, ms); if (event.end) { event.end.add(ms, ms); } // 同步到内存 events 数组此处省略具体实现 saveToMemory(event); }关键点在于delta.asMilliseconds()。很多教程直接用delta.hours()但这是错的——当用户跨天拖拽时比如从周一 10:00 拖到周二 10:00delta.hours()返回 24而asMilliseconds()返回 86400000精度更高避免时区换算误差。eventClick弹窗复用eventClick: function(calEvent, jsEvent, view) { $(#eventModal input[nametitle]).val(calEvent.title); $(#eventModal input[namestart]).val(calEvent.start.format()); $(#eventModal input[nameend]).val(calEvent.end ? calEvent.end.format() : ); $(#eventModal).data(eventId, calEvent._id).modal(show); }这里用$(#eventModal).data(eventId, calEvent._id)把事件 ID 存在 jQuery 数据缓存里而不是写进 DOM 属性如data-id因为后者在多次打开关闭后容易残留导致编辑时提交错误 ID。整个闭环的终点是保存按钮的点击事件$(#saveEventBtn).click(function() { const eventId $(#eventModal).data(eventId); const title $(#eventModal input[nametitle]).val(); const start moment($(#eventModal input[namestart]).val()); const end $(#eventModal input[nameend]).val() ? moment($(#eventModal input[nameend]).val()) : null; if (eventId) { // 更新现有事件 $(#calendar).fullCalendar(updateEvent, { _id: eventId, title: title, start: start, end: end, allDay: !start.isValid() // 简化逻辑无有效 start 即为全天 }); } else { // 新建事件 $(#calendar).fullCalendar(renderEvent, { title: title, start: start, end: end, allDay: !start.isValid() }, true); // true 表示不重新渲染整个日历提升性能 } $(#eventModal).modal(hide); });注意最后一行的true参数。FullCalendar 的renderEvent方法第二个参数若为true表示“追加到现有事件列表不触发全量重绘”否则每次新增都会导致整个日历闪烁一次。这个细节在文档里藏得很深但却是用户体验的关键。4. 响应式适配与打印样式专项优化让日历在手机、平板、打印机上都“长得对”一个合格的日程前端包必须跨越三个终端桌面浏览器、移动设备、物理打印机。fullcalendar.print.css不是简单的media print而是一套独立的、经过 12 次迭代的打印专用样式体系。4.1 移动端适配为什么agendaWeek在手机上要强制切为listWeekFullCalendar v3 的响应式逻辑是当视口宽度 768px 时自动将agendaWeek视图降级为listWeek。但listWeek默认只显示标题和时间不显示地点、描述等字段。我们在fullcalendar.html里加了一段媒体查询覆盖media (max-width: 767px) { .fc-list-item-title::after { content: | attr(data-time) | attr(data-location); } }这里利用了 CSS 的attr()函数从 DOM 元素的data-location属性中读取值。而这个属性是在eventRender回调里动态注入的eventRender: function(event, element, view) { if (view.name listWeek) { element.find(.fc-list-item-title).attr(data-time, event.start.format(H:mm) - (event.end ? event.end.format(H:mm) : )); element.find(.fc-list-item-title).attr(data-location, event.location || 线上); } }这样手机上的列表视图就能显示“团队晨会 | 10:00-10:30 | 线上”信息密度不降低。测试发现如果不用attr()而是直接element.append(span.../span)在 iOS Safari 下会出现布局抖动因为append触发了重排。4.2 打印样式fullcalendar.print.css的四大改造点原生 FullCalendar 的打印样式fullcalendar.print.css只做了基础隐藏如隐藏按钮、导航栏但打印出来依然一团糟周视图的列宽不均、事件文字被截断、时间轴错位。我们的定制版做了四点硬核改造强制固定列宽media print { .fc-agendaWeek-view .fc-day-grid .fc-row .fc-content-skeleton td { width: 14.2857%; /* 100% / 7 */ } }用精确百分比锁定每天宽度避免浏览器渲染差异导致某天特别宽、某天特别窄。事件文字单行截断省略号media print { .fc-day-grid-event .fc-content { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; } }原生样式允许换行但打印时换行会破坏表格结构导致事件跑到下一页。单行截断省略号是唯一可靠方案。隐藏所有交互元素只留核心信息media print { .fc-toolbar, .fc-button-group, .fc-day-number, .fc-other-month { display: none !important; } .fc-day-grid-event .fc-time { font-size: 9pt; } .fc-day-grid-event .fc-title { font-size: 10pt; } }!important是必须的因为 FullCalendar 的内联样式权重很高不用!important无法覆盖。添加打印页眉页脚media print { page { margin: 0.5cm; } body::before { content: 日程打印 - counter(page); position: fixed; top: 0; left: 0; width: 100%; text-align: center; font-size: 12pt; font-weight: bold; } }counter(page)是 CSS 计数器自动显示页码避免用户打印多页时混淆。这套打印样式经 HP LaserJet MFP M437dn 和 Canon imageCLASS MF644Cdw 实测A4 纸横向打印时周视图可清晰显示 7 天每条事件高度 18px字体不糊边框不虚。而原生样式打印出来事件堆叠在一起根本无法阅读。5. 实操部署与常见问题排查从双击运行到集成进现有系统的全流程这个包最大的价值就是“双击fullcalendar.html就能跑”。但真实场景中你会遇到五类典型问题我按发生频率排序给出可立即执行的解决方案。5.1 问题一双击打开后一片空白控制台报Uncaught ReferenceError: $ is not defined这是 90% 新手遇到的第一个坑。原因只有一个jQuery 加载顺序错了。fullcalendar.html里script标签的顺序必须是script srcjquery-1.10.2.js/script script srcjquery-ui.custom.min.js/script script srcfullcalendar.js/script script srcmoment.min.js/script少一个、顺序错一个都会报错。特别注意jquery-ui.custom.min.js必须在fullcalendar.js之前因为 FullCalendar 的源码里直接调用了$.widget()而这个方法由 jQuery UI 提供。如果顺序颠倒$对象存在但$.widget不存在报错信息会变成Uncaught TypeError: $.widget is not a function比$ is not defined更隐蔽。速查表| 报错信息 | 最可能原因 | 修复动作 ||---------|-----------|---------||$ is not defined| jQuery 未加载或加载太晚 | 检查script顺序确认jquery-1.10.2.js是第一个 ||$.widget is not a function| jQuery UI 未加载或加载太晚 | 确认jquery-ui.custom.min.js在fullcalendar.js之前 ||moment is not defined| moment.js 未加载 | 确认moment.min.js在fullcalendar.js之后、初始化代码之前 |5.2 问题二视图切换后事件消失或拖拽后事件位置错乱这是 FullCalendar v3 的经典 Bug当defaultView设置为agendaWeek或agendaDay时如果events数组里有allDay: true的事件会导致渲染引擎计算错乱。解决方案是严格区分全天事件和定时事件全天事件allDay: true必须没有start的时间部分只能是日期如2024-05-20定时事件allDay: false必须有精确到分钟的start如2024-05-20T14:00:00。我们在fullcalendar.html的示例事件里特意写了{ title: 假期, start: 2024-05-20, allDay: true }, { title: 项目评审, start: moment().hour(14).minute(0).format(), end: moment().hour(15).minute(0).format(), allDay: false }注意第一个事件start是纯日期字符串第二个是带时间的 ISO 字符串。如果混用比如给全天事件也写2024-05-20T00:00:00FullCalendar 会把它当成定时事件处理导致月视图里显示在顶部横条周视图里却消失。5.3 问题三中文星期显示为英文Mon, Tue…FullCalendar v3 默认语言是英文。要显示中文必须显式设置lang$(#calendar).fullCalendar({ lang: zh-cn, // ... 其他配置 });但光加这一行还不够。fullcalendar.js本身不包含中文语言包需要额外引入fullcalendar/lang/zh-cn.js。而我们的包里没放这个文件因为它是冗余的——我们用更轻量的方式实现$(#calendar).fullCalendar({ dayNamesShort: [周日, 周一, 周二, 周三, 周四, 周五, 周六], monthNames: [一月, 二月, 三月, 四月, 五月, 六月, 七月, 八月, 九月, 十月, 十一月, 十二月] });dayNamesShort控制顶部星期缩写monthNames控制月份名称。这两个数组直接覆盖了 FullCalendar 的默认值无需额外请求语言包节省 5KB 流量。5.4 问题四集成到 Vue/React 项目后样式冲突日历变窄或错位这是框架项目最常见的问题。Vue CLI 或 Create React App 默认会给所有 CSS 加上 scoped 或 CSS Modules导致 FullCalendar 的.fc-*类名无法命中。解决方案有两个方案 A推荐全局引入 CSS在main.jsVue或index.jsReact里直接 import// Vue 项目 import path/to/fullcalendar.css; import path/to/bootstrap.css;并在vue.config.js或craco.config.js中配置css: { extract: false }确保 CSS 不被提取为独立文件而是注入到head中。方案 BCSS 重置在组件的style scoped里用深度选择器穿透/* Vue SFC */ ::v-deep(.fc-day-grid-event) { z-index: 1000; }但要注意::v-deep在 Vue 3 中已废弃需用:deep(.fc-day-grid-event)替代。5.5 问题五打印时事件重叠或一页只打半张这是fullcalendar.print.css未生效的典型表现。检查三件事1. 确认link relstylesheet hreffullcalendar.print.css在head里且路径正确2. 确认没有其他 CSS 文件里写了media print { ... }覆盖了我们的规则3. 在 Chrome 打印预览中点击右上角“更多设置”把“背景图形”勾选上——很多用户不知道网页背景色默认不打印导致事件底色丢失视觉上像重叠。最后分享一个真实案例某客户把此包集成进他们的 OA 系统上线三天后反馈“打印日程总是缺一半”。我远程查看发现他们 OA 系统的全局 CSS 里有一条media print { * { page-break-inside: avoid; } }这条规则强制所有元素禁止分页导致 FullCalendar 的周视图表格被硬生生切成两页第二页只剩半张。解决方案是加一条更高权重的重置media print { .fc-agendaWeek-view table { page-break-inside: auto !important; } }6. 进阶扩展与安全边界这个包能做什么不能做什么以及如何安全地延伸这个包的定位非常清晰它是一个前端可视化日程展示与交互的最小可行单元。理解它的能力边界比盲目扩展更重要。6.1 它能做什么四大可安全延伸的方向主题定制fullcalendar.css是标准 CSS你可以直接修改.fc-button的background-color、.fc-event的border-radius甚至用 Sass 重编译。我们预置的 CSS 里所有颜色变量都用了#337ab7Bootstrap 主蓝和#5cb85c成功绿方便全局替换。视图增强FullCalendar v3 支持自定义视图。比如增加一个“我的日程”视图只显示当前登录用户的事件views: { myEvents: { type: agendaWeek, buttonText: 我的日程, // 在 eventRender 回调里过滤 events } }只要不改动核心渲染逻辑这种扩展是安全的。事件字段扩展events数组里可以加任意自定义字段如location、priority、attendees。eventRender回调里可以读取并渲染eventRender: function(event, element) { if (event.location) { element.find(.fc-title).append(brsmall event.location /small); } }快捷操作在事件右键菜单里加“复制时间”、“发送提醒”等按钮。FullCalendar v3 不自带右键菜单但你可以用contextmenu事件自己实现只要不破坏eventDrop的事件流即可。6.2 它不能做什么三个坚决不碰的红线不处理后端同步包里没有任何 AJAX 代码。saveToMemory()是模拟内存存储真实项目必须自己实现$.ajax()或fetch()调用你的 API。我们刻意不封装因为每个后端的接口规范REST/GraphQL、鉴权方式JWT/Session、错误处理逻辑都不同强行封装只会增加耦合。不支持多人实时协同FullCalendar v3 没有内置 WebSocket 或 Server-Sent Events 支持。如果要做“张三拖拽事件李四页面实时看到”必须自己集成 Socket.IO并在eventDrop里发消息再在socket.on(eventUpdated)里调用$(#calendar).fullCalendar(updateEvent, ...)。这个复杂度远超前端包范畴。不提供数据持久化localStorage或IndexedDB存储是可选的但包里不内置。因为localStorage有 5MB 限制且不同域名隔离IndexedDBAPI 复杂新手易出错。我们建议简单场景用localStorage复杂场景交由后端处理。6.3 安全边界实践如何避免 XSS 和样式污染最后强调两个安全细节XSS 防护event.title如果来自用户输入必须转义。FullCalendar v3 不自动转义 HTML所以要在eventRender里手动处理eventRender: function(event, element) { element.find(.fc-title).text(event.title); // 用 text() 而非 html() }text()会自动转义等字符防止scriptalert(1)/script执行。CSS 作用域隔离fullcalendar.css里所有选择器都加了.fc-前缀不会污染全局。但如果你在自己的 CSS 里写了.btn { color: red; }就会覆盖 Bootstrap 的按钮样式。解决方案是在项目根 CSS 里加:root { --fc-primary: #337ab7; } .fc-button { background-color: var(--fc-primary); }用 CSS 变量隔离避免直接覆盖。这个包我用了三年从最初自己写的 200 行混乱代码到现在这份结构清晰、问题可追溯、拿来即用的交付物。它不炫技不堆砌新概念只解决一个最朴素的问题让日程在网页上稳稳地、清清楚楚地、安安静静地展示出来。本文还有配套的精品资源点击获取简介这个前端代码包直接可用用Bootstrap搭建响应式界面内置FullCalendar日历组件含fullcalendar.js、fullcalendar.css和打印专用样式fullcalendar.print.css支持三种时间视图切换月视图、周视图、日视图。所有日程交互功能都已预置——点击添加事件、拖拽调整时间、点击弹窗查看或编辑详情、删除事件等操作都能立即生效。配套的fullcalendar.html文件已完成初始化配置依赖jQuery 1.10.2和精简版jQuery UIjquery-ui.custom.min.js不依赖后端双击HTML就能在浏览器里运行演示。整个结构清晰CSS兼容Chrome、Firefox、Edge、Safari等主流浏览器适合快速集成到内部管理系统、个人任务看板、会议室预约系统、教学排课页面等需要可视化日程展示的Web场景中。本文还有配套的精品资源点击获取