FullCalendar时区异常求助:加载时间较原输入快5小时
问题现象
前端通过下拉菜单选择时区后,将对应带偏移量的ISO格式时间发送至Django服务器,服务器端存储的时间完全正确,但加载到FullCalendar时,显示的时间比原输入时间快5小时(对应America/Chicago CST时区,偏移量为-5)。尝试移除FullCalendar实例配置中的timeZone选项后,问题依然存在。
相关代码
前端代码(calendar.html)
var calendar = $("#calendar").fullCalendar({ // timeZone: $("#id_timezone").val(), timeZone: "UTC", // UTC仅用于测试 header: { left: "prev,next today", center: "title", right: "month,agendaWeek,agendaDay", }, events: "/all_events", selectable: true, selectHelper: true, editable: true, eventLimit: true, eventDurationEditable: true, select: function (start, end, allDay) { var title = prompt("Enter Event Title"); if (title) { var start = $.fullCalendar.formatDate(start, "Y-MM-DD HH:mm:ss"); var end = $.fullCalendar.formatDate(end, "Y-MM-DD HH:mm:ss"); $.ajax({ type: "GET", url: "/add_event", data: { title: title, start: start, end: end, timezone: $("#id_timezone").val(), }, dataType: "json", success: function (data) { calendar.fullCalendar("refetchEvents"); alert("Added Successfully " + start); }, error: function (data) { alert( "There is a problem!!! " + start + $("#timezone").val() ); }, }); } }, eventResize: function (event) { var start = $.fullCalendar.formatDate( event.start, "Y-MM-DD HH:mm:ss" ); var end = $.fullCalendar.formatDate(event.end, "Y-MM-DD HH:mm:ss"); var title = event.title; var id = event.id; $.ajax({ type: "GET", url: "/update", data: { title: title, start: start, end: end, id: id, timezone: $("#id_timezone").val(), }, dataType: "json", success: function (data) { calendar.fullCalendar("refetchEvents"); alert("Event Update"); }, error: function (data) { alert("There is a problem!!!"); }, }); }, eventDrop: function (event) { var start = $.fullCalendar.formatDate( event.start, "Y-MM-DD HH:mm:ss" ); var end = $.fullCalendar.formatDate(event.end, "Y-MM-DD HH:mm:ss"); var title = event.title; var id = event.id; $.ajax({ type: "GET", url: "/update", data: { title: title, start: start, end: end, id: id, timezone: $("#id_timezone").val(), }, dataType: "json", success: function (data) { calendar.fullCalendar("refetchEvents"); alert("Event Update"); }, error: function (data) { alert("There is a problem!!!"); }, }); }, eventClick: function (event) { $("#eventModal").modal("show"); $("#modalTitle").val(event.title); const dayStart = $.fullCalendar.formatDate(event.start, "Y-MM-DD"); const dayEnd = $.fullCalendar.formatDate(event.end, "Y-MM-DD"); var timeStart = $.fullCalendar.formatDate(event.start, "HH:mm:ss"); var timeEnd = $.fullCalendar.formatDate(event.start, "HH:mm:ss"); $("#dayStart").val(dayStart); $("#dayEnd").val(dayEnd); $("timeStart").val(timeStart); $("timeEnd").val(timeEnd); $("#desc").val(event.desc); $("#saveEvent").on("click", function () { var id = event.id; $.ajax({ type: "GET", url: "/updateEvent", data: { id: id, title: $("#modalTitle").val(), dayStart: $("#dayStart").val(), dayEnd: $("#dayEnd").val(), timeStart: $("#timeStart").val(), timeEnd: $("#timeEnd").val(), desc: $("#desc").val(), timezone: $("#id_timezone").val(), }, dataType: "json", success: function (id) { calendar.fullCalendar("refetchEvents"); alert("Event Updated"); }, error: function (data) { alert("There is a problem!!! " + id); }, }); }); $("#removeBtn").on("click", function () { var id = event.id; $.ajax({ type: "GET", url: "/remove", data: { id: id }, dataType: "json", success: function (data) { calendar.fullCalendar("refetchEvents"); alert("Event Removed"); }, error: function (data) { alert("There is a problem!!!"); }, }); }); }, });
后端代码(views.py)
def all_events(request): user = request.user.id user_events = Events.objects.filter(user=user) program_events = Events.objects.filter(user__isnull=True) out = [] tz = afbUsers.objects.get(pk=user) timezone = pytz.timezone(tz.timezone) for event in user_events: # start = event.start.strftime('%Y-%m-%d %H:%M:%S') # print(start) # start = parser.parse(start) # start = start.astimezone(timezone) # print(start) # end = event.end.strftime('%Y-%m-%d %H:%M:%S') # end = parser.parse(end) # end = end.astimezone(timezone) out.append({ 'title': event.name, 'id': event.id, 'start': event.start.strftime("%m/%d/%Y, %H:%M:%S"), 'end': event.end.strftime("%m/%d/%Y, %H:%M:%S"), }) # for event in program_events: # start = event.start # start = start.astimezone(utc_timezone) # end = event.end # end = end.astimezone(utc_timezone) # out.append({ # 'title': event.name, # 'id': event.id, # 'start': start.strftime("%m/%d/%Y, %H:%M:%S"), # 'end': end.strftime("%m/%d/%Y, %H:%M:%S"), # }) return JsonResponse(out, safe=False)
数据示例
{ "id": 1, "user": 42, // 对应afbUsers表中已存在的用户ID "name": "Community Meetup", "start": "2024-04-26T18:00:00Z", "end": "2024-04-26T20:00:00Z", "desc": "A gathering for the local community to discuss upcoming projects and initiatives." }
问题根源与解决方案
1. 后端返回时间格式缺失时区信息
目前后端将时间格式化为%m/%d/%Y, %H:%M:%S,这是不带时区标识的本地时间字符串,FullCalendar无法识别其所属时区,会默认使用浏览器本地时区解析,导致与用户选择的时区出现偏差。
修复方案:
将后端返回的时间改为带时区信息的ISO 8601格式,或者UTC格式的ISO字符串。修改views.py中的时间格式化代码:
# 替换原有的start和end格式化代码 'start': event.start.isoformat(), 'end': event.end.isoformat(),
如果数据库中存储的是UTC时间,也可以直接返回UTC的ISO字符串,确保FullCalendar能正确解析时区。
2. FullCalendar的timeZone配置错误
当前代码中timeZone被硬编码为"UTC"用于测试,实际应该设置为用户选择的时区(即$("#id_timezone").val()),让FullCalendar基于用户选择的时区渲染事件。
修复方案:
恢复注释掉的配置,移除测试用的UTC设置:
timeZone: $("#id_timezone").val(), // timeZone: "UTC", // 移除这行测试代码
3. 前端日期格式化丢失时区信息
在select、eventResize、eventDrop等事件中,使用$.fullCalendar.formatDate将时间转为Y-MM-DD HH:mm:ss格式,同样丢失了时区信息,导致后端接收到的时间无法正确关联用户选择的时区。
修复方案:
直接使用FullCalendar提供的原生时间对象的toISOString()方法,保留时区信息:
// 替换select事件中的start、end格式化 var start = start.toISOString(); var end = end.toISOString();
或者在格式化时指定时区:
var start = $.fullCalendar.formatDate(start, "Y-MM-DDTHH:mm:ssZ", {timeZone: $("#id_timezone").val()}); var end = $.fullCalendar.formatDate(end, "Y-MM-DDTHH:mm:ssZ", {timeZone: $("#id_timezone").val()});
4. 额外注意事项
- 确保Django模型中的
start和end字段使用DateTimeField并开启auto_now_add或手动设置带时区的时间,存储为UTC时间。 - 前端发送请求时,务必将用户选择的时区一并传递给后端,后端处理时基于该时区转换时间后存储。
内容的提问来源于stack exchange,提问作者Hoko L

