You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何通过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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.12 22:55:20