如何通过FullCalendar、Laravel和Vue3的@click方法实现日历事件增删改
解决方案
要实现日历事件的CRUD操作同步到数据库,你需要启用FullCalendar的eventAdd、eventChange、eventRemove事件钩子,在这些钩子中调用后端API,并确保操作失败时回滚日历视图,避免数据不一致。以下是具体实现步骤:
步骤1:补全事件ID生成函数
如果没有导入官方的event-utils.ts,可以自己实现一个简单的ID生成函数,放在<script setup>中:
const createEventId = () => { return Math.random().toString(36).substring(2, 11); };
步骤2:添加数据库同步钩子到日历配置
在calendarOptions中启用三个核心事件钩子,用于监听事件的创建、修改和删除:
calendarOptions: { // ... 其他已有配置 select: this.handleDateSelect, eventClick: this.handleEventClick, eventsSet: this.handleEvents, // 新增以下三个钩子 eventAdd: this.handleEventAdd, eventChange: this.handleEventChange, eventRemove: this.handleEventRemove, // ... 其他已有配置 }
步骤3:实现API同步方法
在methods中添加三个异步方法,分别处理事件的创建、更新和删除请求,并在失败时回滚日历操作:
// 处理事件创建 async handleEventAdd(addInfo: EventAddArg) { try { await axios.post('store-event', { id: addInfo.event.id, title: addInfo.event.title, start: addInfo.event.startStr, end: addInfo.event.endStr, allDay: addInfo.event.allDay }); console.log('事件创建成功'); } catch (error) { addInfo.event.remove(); // 回滚日历操作 alert('事件创建失败,请重试'); console.error(error); } }, // 处理事件修改(拖拽/调整大小) async handleEventChange(changeInfo: EventChangeArg) { try { await axios.put(`update-event/${changeInfo.event.id}`, { title: changeInfo.event.title, start: changeInfo.event.startStr, end: changeInfo.event.endStr, allDay: changeInfo.event.allDay }); console.log('事件更新成功'); } catch (error) { changeInfo.revert(); // 回滚日历操作 alert('事件更新失败,请重试'); console.error(error); } }, // 处理事件删除 async handleEventRemove(removeInfo: EventRemoveArg) { try { await axios.delete(`delete-event/${removeInfo.event.id}`); console.log('事件删除成功'); } catch (error) { removeInfo.view.calendar.addEvent(removeInfo.event); // 回滚日历操作 alert('事件删除失败,请重试'); console.error(error); } }
步骤4:修正现有方法细节
确保handleDateSelect中的ID生成正常,handleEventClick的删除逻辑会触发eventRemove钩子:
handleDateSelect(selectInfo: DateSelectArg) { let title = prompt('Please enter a new title for your event') let calendarApi = selectInfo.view.calendar calendarApi.unselect() // 清除日期选择 if (title) { calendarApi.addEvent({ id: createEventId(), title, start: selectInfo.startStr, end: selectInfo.endStr, allDay: selectInfo.allDay }) } }, handleEventClick(clickInfo: EventClickArg) { if (confirm(`Are you sure you want to delete the event '${clickInfo.event.title}'`)) { clickInfo.event.remove() // 触发eventRemove钩子 } }
完整修改后的代码
<template> <head title="Dashboard" /> <BreezeAuthenticatedLayout> <template #header> <h2 class="font-semibold text-xl text-gray-800 leading-tight"> Resource Calendar Timeline </h2> </template> <div class="py-12"> <div class="max-w-10xl mx-auto sm:px-6 lg:px-8"> <div class="bg-white overflow-hidden shadow-sm sm:rounded-lg"> <div class="p-12 bg-white border-b border-gray-200"> <!--start calendar--> <div class='demo-app'> <div class='demo-app-sidebar'> <div class='demo-app-sidebar-section'> <h2>Instructions</h2> <ul> <li>Select dates and you will be prompted to create a new event</li> <li>Drag, drop, and resize events</li> <li>Click an event to delete it</li> </ul> </div> <div class='demo-app-sidebar-section'> <label> <input type='checkbox' :checked='calendarOptions.weekends' @change='handleWeekendsToggle' /> toggle weekends </label> </div> <div class='demo-app-sidebar-section'> <h2>All Events ({{ currentEvents.length }})</h2> <ul> <li v-for='event in currentEvents' :key='event.id'> <b>{{ event.startStr }}</b> <i>{{ event.title }}</i> </li> </ul> </div> </div> <div class='demo-app-main'> <FullCalendar class='demo-app-calendar' :options='calendarOptions'> <template v-slot:eventContent='arg'> <b>{{ arg.timeText }}</b> <i>{{ arg.event.title }}</i> </template> </FullCalendar> </div> </div> </div> </div> </div> </div> </BreezeAuthenticatedLayout> </template> <script setup lang='ts'> import BreezeAuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue'; import { head, Link } from '@inertiajs/inertia-vue3'; import '@fullcalendar/core/vdom'; import FullCalendar from '@fullcalendar/vue3'; import dayGridPlugin from '@fullcalendar/daygrid'; import timeGridPlugin from '@fullcalendar/timegrid'; import listPlugin from '@fullcalendar/list'; import resourceTimelinePlugin from '@fullcalendar/resource-timeline'; import interactionPlugin from '@fullcalendar/interaction'; import axios from 'axios'; // 自定义事件ID生成函数 const createEventId = () => { return Math.random().toString(36).substring(2, 11); }; </script> <script lang='ts'> import type { CalendarOptions, DateSelectArg, EventClickArg, EventApi, EventAddArg, EventChangeArg, EventRemoveArg } from '@fullcalendar/core'; export default { components: {FullCalendar}, data() { return { calendarOptions: { plugins: [ dayGridPlugin, timeGridPlugin, resourceTimelinePlugin, listPlugin, interactionPlugin ], events: [], headerToolbar: { left: 'promptResource prev,next today', center: 'title', right: 'resourceMonth,dayGridMonth,timeGridWeek,timeGridDay,listMonth' }, views: { resourceMonth: { type: 'resourceTimelineMonth', buttonText: 'personnel' } }, customButtons: { promptResource: { text: "+ personnel", click: function() { var title = prompt("Name"); if (title) { const calendar = this; calendar.addResource({ title: title }); fetch("add_resources.php", { method: "POST", headers: { Accept: "application/json" }, body: new URLSearchParams({ title: title }) }) .then(response => console.log(response)) .catch(error => console.log(error)); } } } }, initialView: 'resourceTimelineMonth', editable: true, selectable: true, selectMirror: true, dayMaxEvents: true, weekends: true, select: this.handleDateSelect, eventClick: this.handleEventClick, eventsSet: this.handleEvents, // 启用数据库同步钩子 eventAdd: this.handleEventAdd, eventChange: this.handleEventChange, eventRemove: this.handleEventRemove, resourceAreaHeaderContent: 'Personnel', filterResourcesWithEvents: false, resourceGroupField: 'building', resources: [], } as CalendarOptions, currentEvents: [] as EventApi[], } }, created() { this.getEvents(); this.getResources(); }, methods: { getEvents() { axios.get('show-events') .then(response => { this.calendarOptions.events = response.data.events; }); }, getResources() { axios.get('show-resources') .then(response => { this.calendarOptions.resources = response.data.resources; }); }, handleWeekendsToggle() { this.calendarOptions.weekends = !this.calendarOptions.weekends }, handleDateSelect(selectInfo: DateSelectArg) { let title = prompt('Please enter a new title for your event') let calendarApi = selectInfo.view.calendar calendarApi.unselect() if (title) { calendarApi.addEvent({ id: createEventId(), title, start: selectInfo.startStr, end: selectInfo.endStr, allDay: selectInfo.allDay }) } }, handleEventClick(clickInfo: EventClickArg) { if (confirm(`Are you sure you want to delete the event '${clickInfo.event.title}'`)) { clickInfo.event.remove() } }, handleEvents(events: EventApi[]) { this.currentEvents = events }, // 新增:事件创建同步 async handleEventAdd(addInfo: EventAddArg) { try { await axios.post('store-event', { id: addInfo.event.id, title: addInfo.event.title, start: addInfo.event.startStr, end: addInfo.event.endStr, allDay: addInfo.event.allDay }); console.log('Event created successfully'); } catch (error) { addInfo.event.remove(); alert('Failed to create event. Please try again.'); console.error(error); } }, // 新增:事件修改同步 async handleEventChange(changeInfo: EventChangeArg) { try { await axios.put(`update-event/${changeInfo.event.id}`, { title: changeInfo.event.title, start: changeInfo.event.startStr, end: changeInfo.event.endStr, allDay: changeInfo.event.allDay }); console.log('Event updated successfully'); } catch (error) { changeInfo.revert(); alert('Failed to update event. Please try again.'); console.error(error); } }, // 新增:事件删除同步 async handleEventRemove(removeInfo: EventRemoveArg) { try { await axios.delete(`delete-event/${removeInfo.event.id}`); console.log('Event deleted successfully'); } catch (error) { removeInfo.view.calendar.addEvent(removeInfo.event); alert('Failed to delete event. Please try again.'); console.error(error); } } } } </script> <style lang='css'> h2 { margin: 0; font-size: 16px; } ul { margin: 0; padding: 0 0 0 1.5em; } li { margin: 1.5em 0; padding: 0; } b { /* used for event dates/times */ margin-right: 3px; } .demo-app { display: flex; min-height: 100%; font-family: Arial, Helvetica Neue, Helvetica, sans-serif; font-size: 14px; } .demo-app-sidebar { width: 300px; line-height: 1.5; background: #eaf9ff; border-right: 1px solid #d3e2e8; } .demo-app-sidebar-section { padding: 2em; } .demo-app-main { flex-grow: 1; padding: 3em; } .fc { /* the calendar root */ max-width: 1200px; margin: 0 auto; } .fc-day-today { background-color: var(--fc-today-bg-color, rgba(255, 220, 40, 0.15)); } .fc-day-sat, .fc-day-sun { background-color: var(--fc-today-bg-color, rgba(255, 100, 40, 0.15)); } </style>
关键说明
- 请将API路径(
store-event、update-event、delete-event)替换为你实际的后端接口地址 - 所有API请求使用
async/await确保异步操作顺序,失败时会自动回滚日历视图,避免数据不一致 - 补全了TypeScript类型定义,保证代码类型安全
内容的提问来源于stack exchange,提问作者Steve Adams
相关产品推荐
相关产品推荐

