FullCalendar是jQuery生态里最经典的日历组件之一,它不仅渲染效果好,还内置了事件拖拽、事件缩放、日期点击等交互能力,非常适合用来做会议室预订、个人日程、排班系统这类应用。不过很多初学者会发现一个现象:把日程从一个日期拖到另一个日期,页面上的显示确实变了,可是一刷新,事件又回到了原来的位置。这是因为拖拽只修改了前端数据,服务器根本不知道这次变更。本文就围绕这个核心问题,讲清楚如何在jQuery项目中接入FullCalendar,并把拖拽产生的变更可靠地持久化到后端。

一、引入插件并完成基础初始化
FullCalendar的引入方式有两种,一种是通过CDN直接引用css和js文件,另一种是npm安装后打包引入。对于传统的jQuery项目,CDN方式最省事。需要注意的是,FullCalendar从v3升级到v5之后API变化很大,v5改用ES模块导出,全局初始化写法变成了FullCalendar.Calendar,而不再支持$('#calendar').fullCalendar()这种链式调用。如果你的项目是老系统改造,建议直接使用v3.10版本,它的jQuery风格API与大量网上资料一致,踩坑最少。
以v3为例,引入资源后,先准备一个容器div,再在document ready里初始化。最简配置包含defaultView(视图类型)、events(事件数据源)和header(头部按钮),代码如下:
<link href="https://cdn.jsdelivr.net/npm/fullcalendar@3.10.5/dist/fullcalendar.min.css" rel="stylesheet"/>
<script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@3.10.5/dist/fullcalendar.min.js"></script>
<div id="calendar"></div>
<script>
$(function () {
$('#calendar').fullCalendar({
defaultView: 'month',
header: {
left: 'prev,next today',
center: 'title',
right: 'month,agendaWeek,agendaDay'
},
// 事件数据从后端接口加载
events: '/api/events',
editable: true
});
});
</script>
</script>这里的events参数很灵活,可以直接传数组、JSON接口地址,也可以传一个函数自己控制Ajax请求。对于日程管理系统,推荐传接口地址,让后端返回标准的JSON数组,每个事件至少包含id、title、start三个字段,如果有结束时间再加end。后端返回的时间格式建议统一为YYYY-MM-DD HH:mm:ss,FullCalendar可以正常解析。
二、开启拖拽并捕获eventDrop与eventResize回调
拖拽能力的总开关是editable: true,不设置这个参数,事件在页面上是拖不动的。开启之后还要注意两个细分的开关:eventStartEditable控制事件能否被拖动到别的日期,eventDurationEditable控制事件能否被拉伸改变时长。有些系统希望允许移动但不允许改时长,就可以单独把eventDurationEditable设为false,这种精细控制在排班场景里很实用。
用户把事件拖到新位置后,组件会触发eventDrop回调;拉伸事件的边缘改变起止时间,则触发eventResize。这两个回调都会把事件对象、新的起止时间以及revertFunc回滚函数交给你。持久化的关键就在这两个回调里:先通过Ajax把新时间提交给后端,如果后端返回失败,就调用revertFunc()把事件弹回原位,这样用户能立刻感知保存失败,而不是等刷新后才发现数据丢了。
$('#calendar').fullCalendar({
defaultView: 'month',
events: '/api/events',
editable: true,
// 拖动事件到新的日期或时间
eventDrop: function (event, delta, revertFunc) {
$.ajax({
url: '/api/events/update',
type: 'POST',
dataType: 'json',
data: {
id: event.id,
// moment格式化,保证后端拿到标准时间字符串
start: event.start.format('YYYY-MM-DD HH:mm:ss'),
end: event.end ? event.end.format('YYYY-MM-DD HH:mm:ss') : null
},
success: function (res) {
if (res.code !== 0) {
revertFunc(); // 保存失败,把事件还原到拖拽前的位置
alert(res.msg || '保存失败,请重试');
}
},
error: function () {
revertFunc();
alert('网络异常,变更未保存');
}
});
},
// 拉伸边缘改变事件时长
eventResize: function (event, delta, revertFunc) {
$.ajax({
url: '/api/events/update',
type: 'POST',
data: {
id: event.id,
start: event.start.format('YYYY-MM-DD HH:mm:ss'),
end: event.end.format('YYYY-MM-DD HH:mm:ss')
},
success: function (res) {
if (res.code !== 0) {
revertFunc();
}
},
error: function () {
revertFunc();
}
});
}
});除了拖拽已有事件,新建日程一般通过dayClick或eventClick实现:前者在用户点击空白日期格子时弹出表单,把选中的日期作为默认开始时间;后者在点击事件时打开编辑弹窗。两个回调配合Bootstrap的模态框或者Layer弹层,就能完成日程的增删改闭环。要注意dayClick回调里的date参数是一个moment对象,直接date.format()即可得到用户点击的时间点。
三、后端接口设计与数据持久化
前端的持久化只是把数据发出去,真正落库还得靠后端。事件表的设计建议包含以下字段:id(主键)、title(标题)、start_time(开始时间)、end_time(结束时间)、all_day(是否全天事件)、color(自定义颜色)、user_id(归属人)以及created_at、updated_at两个时间戳。其中all_day字段不要省略,FullCalendar在月视图下把事件拖成全天事件时,event.allDay会变为true,此时end的语义是排他的,后端存储时要处理好这种差异。
更新接口的逻辑很简单:接收id和新的起止时间,更新对应记录。以PHP配合MySQL为例,接口代码可以这样写:
<?php
// /api/events/update
$data = json_decode(file_get_contents('php://input'), true);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$data = $_POST;
}
$id = isset($data['id']) ? intval($data['id']) : 0;
$start = isset($data['start']) ? $data['start'] : '';
$end = isset($data['end']) ? $data['end'] : null;
if ($id <= 0 || $start === '') {
echo json_encode(['code' => 1, 'msg' => '参数错误']);
exit;
}
// 使用预处理语句防止SQL注入
$stmt = $pdo->prepare("UPDATE calendar_events SET start_time = ?, end_time = ? WHERE id = ?");
$ok = $stmt->execute([$start, $end, $id]);
echo json_encode($ok ? ['code' => 0] : ['code' => 1, 'msg' => '更新失败']);有一点经常被忽略:更新接口一定要校验当前用户是否有权修改这条事件。如果只凭前端传来的id就更新,任何登录用户都可以改别人的日程,属于典型的越权漏洞。正确做法是在WHERE条件里加上user_id,或者先查询记录归属再判断权限。另外建议给接口加乐观锁或至少记录updated_at,便于后续做冲突检测。
列表接口返回的JSON要注意格式规范,字段名与FullCalendar对应,返回前把数据库里的时间字段格式化好,前端就无需二次处理。示例如下:
$rows = $pdo->query("SELECT id, title, start_time, end_time, color FROM calendar_events WHERE user_id = {$uid}")->fetchAll(PDO::FETCH_ASSOC);
$events = array_map(function ($row) {
return [
'id' => $row['id'],
'title' => $row['title'],
'start' => $row['start_time'],
'end' => $row['end_time'],
'color' => $row['color']
];
}, $rows);
header('Content-Type: application/json');
echo json_encode($events);四、常见问题排查:拖拽不生效、时区偏移与保存回滚
拖拽不生效排第一位的原因永远是忘了设置editable: true。其次是事件数据里缺少id,没有id的事件FullCalendar视为临时对象,某些版本下无法正确响应拖拽后的更新。还有一种情况是容器外层CSS设置了pointer-events: none或者被透明遮罩层盖住,导致事件元素收不到鼠标事件,用浏览器开发者工具检查元素层级即可定位。
时区问题也相当常见。典型症状是拖拽保存后刷新,事件偏移了8小时,这基本就是浏览器本地时间与服务器时区不一致导致的。解决办法有三种:初始化时设置timezone: 'local'并让后端统一存储不带时区的时间字符串;或者后端全部使用UTC存储,前端通过moment的timezone插件做转换;再或者最简单粗暴,前后端都固定用同一个时区(比如统一东八区)存取,不引入任何时区换算。对小规模内部系统,第三种方案最省心。
关于保存回滚,再补充两个实践建议。第一,Ajax请求尽量用POST并带上CSRF令牌,避免接口被跨站伪造请求调用。第二,如果业务上存在并发修改的可能,比如两个运营同时调整同一个会议的时间,可以在事件表加一个version字段,更新时校验版本号,版本不一致就返回失败并触发前端的revertFunc,同时提示用户刷新后重试。这样配合FullCalendar自带的回滚机制,就能保证界面显示与数据库状态始终一致,用户体验和数据准确性都得到了保障。
FullCalendarjQuery日程管理事件拖拽修改时间:2026-09-04 15:20:56