Fullcalendar这个插件挺好,就是很多方法感觉官方文档也没怎么说,导致上手难度大,而且有些默认事件真的不太友好...废话不多说,先上效果图!

1、效果GIF 

1.1 基本按钮功能

1.2  事件hover显示

 

1.3 事件添加、编辑、状态修改 

 

1.4 日历事件搜索 

 

2、 代码实现

 2.1 Fullcalendar日历、el-popover弹窗

        Fullcalendar@5.11.3引入后,要设置一大堆参数calendarOptions,包括显示时间区域、默认视图、是否显示全天类型、中文界面、事件的操作函数等,具体的一些设置内容,见下面代码的注释。

<template>         
 <FullCalendar
            class="calendar"
            ref="fullCalendar"
            :options="calendarOptions"
            ><template v-slot:eventContent="arg">
              <el-popover
                :append-to-body="true"
                ref="popover1"
                placement="top-start"
                width="220"
                :visible-arrow="true"
                trigger="hover"
                :teleported="false"
                popper-class="popover"
                :open-delay="100"
                @show="showPic(arg)"
                @hide="popoverPicReset(arg)"
              >
                <el-row class="popover_title">
                  <el-col
                    :span="12"
                    :style="{
                      color:
                        arg.event.extendedProps.isDone == false
                          ? 'red'
                          : 'green',
                    }"
                  >
                    <span
                      style="padding-right: 2px"
                      :style="{
                        'border-left':
                          arg.event.extendedProps.isDone == false
                            ? '5px solid red'
                            : '5px solid green',
                      }"
                    ></span
                    >{{
                      arg.event.extendedProps.isDone == false
                        ? "未开始"
                        : "已完成"
                    }}</el-col
                  >
                  <el-col
                    :span="12"
                    style="
                      display: flex;
                      flex-direction: row-reverse;
                      font-size: 14;
                      color: #000;
                    "
                    >{{
                      arg.event.allDay == true
                        ? "全天"
                        : formatTimer(arg.event.start)
                    }}</el-col
                  >
                </el-row>
                <el-row>
                  <el-col :span="24" style="text-align: center">
                    <el-image
                      v-if="popoverimg.length != 0"
                      :src="popoverimg[0]"
                      @click="PreviewPic(popoverimg)"
                      fit="fill"
                      class="popoverShowImg"
                      ><div slot="placeholder" class="image-slot">
                        加载中<span class="dot">...</span>
                      </div></el-image
                    >
                    <div class="block"></div>
                  </el-col>
                </el-row>
                <el-row class="popover_content">
                  <el-col :span="24" style="max-height: 150px; overflow: auto">
                    <span class="click">{{ arg.event.title }}</span>
                  </el-col>
                  <el-col :span="24">
                    <el-link
                      v-if="
                        arg.event.extendedProps.address != null &&
                        arg.event.extendedProps.address != ''
                      "
                      :href="undefined"
                      :underline="false"
                      @click="fileDownload(arg.event.extendedProps.address)"
                      class="link"
                      >{{
                        arg.event.extendedProps.address == null
                          ? ""
                          : arg.event.extendedProps.address.replace(
                              "D:\\flask\\upload\\",
                              ""
                            )
                      }}</el-link
                    ></el-col
                  >
                </el-row>

                <el-row style="margin-top: 5px"
                  ><el-col style="width: 15%"
                    ><div>
                      <el-button
                        class="hvr-icon-pulse-grow"
                        :popperAppendToBody="false"
                        size="mini"
                        icon="el-icon-edit hvr-icon"
                        type="primary"
                        circle
                        @click="handleEventClick(arg)"
                      >
                      </el-button>
                    </div>
                  </el-col>
                  <el-col style="width: 15%"
                    ><el-button
                      class="hvr-icon-bounce"
                      size="mini"
                      type="success"
                      icon="el-icon-document-checked hvr-icon"
                      circle
                      @click="onCheckBtnClicked(arg)"
                    >
                    </el-button>
                  </el-col>
                  <el-col style="width: 15%"
                    ><el-popconfirm
                      confirm-button-text="好"
                      cancel-button-text="否"
                      icon="el-icon-info"
                      icon-color="red"
                      title="确定删除这个事项吗?"
                      @confirm="onRemoveBtnClicked(arg)"
                      ><el-button
                        class="hvr-icon-buzz-out"
                        slot="reference"
                        size="mini"
                        type="danger"
                        icon="el-icon-delete hvr-icon"
                        circle
                      >
                      </el-button
                    ></el-popconfirm> </el-col
                ></el-row>
                <div slot="reference">
                  <span class="tree_span_text">{{ arg.timeText }}</span>
                  <span>{{ arg.event.title }}</span>
                </div>
              </el-popover>
            </template>
          </FullCalendar>
</template>    
  <script>
import FullCalendar from "@fullcalendar/vue";
import dayGridPlugin from "@fullcalendar/daygrid";
import timeGridPlugin from "@fullcalendar/timegrid";
import interactionPlugin from "@fullcalendar/interaction";
import listPlugin from "@fullcalendar/list";
  export default {
  // 注册局部组件
  components: {
    FullCalendar,
    Treeselect,
    "el-image-viewer": () =>
      import("element-ui/packages/image/src/image-viewer"),
  },
    data() {
    return {
// Fullcalendar版本@5.11.3日历控件设置项,官网文档地址:https://fullcalendar.io/
      calendarOptions: {
        visibleRange: { start: "2000-01-01", end: "2100-12-31" }, // 可视化区间,必须设置,否则查询列表不会显示事件
        // validRange: { start: "2021-09-01", end: "2021-09-01" }, // 可展示区间
        // 引入的插件,比如fullcalendar/daygrid,fullcalendar/timegrid引入后才可显示月,周,日
        plugins: [dayGridPlugin, interactionPlugin, timeGridPlugin, listPlugin],
        initialView: "dayGridMonth", // 默认为那个视图(月:dayGridMonth,周:timeGridWeek,日:timeGridDay)
        firstDay: 1, // 设置一周中显示的第一天是哪天,周日是0,周一是1,类推
        locale: "zh-cn", // 切换语言,当前为中文
        allDaySlot: true, // 不显示all-day
        businessHours: true, //
        handleWindowResize: true, // 是否随浏览器窗口大小变化而自动变化。
        allDayText: "全天", // 设置all-Day显示的文字,不设置的话默认显示"all-Day"
        themeSystem: "bootstrap", // 主题色(本地测试未能生效)
        // loading: this.loadingEvent, // 视图数据加载中、加载完成触发(用于配合显示/隐藏加载指示器。)
        // selectAllow: this.selectAllow, //编程控制用户可以选择的地方,返回true则表示可选择,false表示不可选择
        // eventMouseEnter: this.eventMouseEnter, // 鼠标滑过
        allowContextMenu: false,
        editable: true, // 是否可以进行(拖动、缩放)修改
        eventStartEditable: true, // Event日程开始时间可以改变,默认true,如果是false其实就是指日程块不能随意拖动,只能上下拉伸改变他的endTime
        eventDurationEditable: true, // Event日程的开始结束时间距离是否可以改变,默认true,如果是false则表示开始结束时间范围不能拉伸,只能拖拽
        selectable: true, // 是否可以选中日历格
        selectMirror: true,
        selectMinDistance: 0, // 选中日历格的最小距离
        // eventLimit: true, //数据条数太多时,限制各自里显示的数据条数(多余的以“+2more”格式显示),默认false不限制,支持输入数字设定固定的显示条数
        moreLinkContent: "+ 更多", //当一块区域内容太多以"+2 more"格式显示时,这个more的名称自定义
        // dayPopoverFormat: "YYYY-M-d",
        dayMaxEventRows: true, // 日历显示事件最大条数,for all non-TimeGrid views
        weekends: true, //
        navLinks: true, // 天链接
        selectHelper: false,
        slotEventOverlap: false, // 相同时间段的多个日程视觉上是否允许重叠,默认true允许
        aspectRatio: 1.35, //设置日历单元格宽度与高度的比例。
        expandRows: true,
        height: auto,
        contentHeight: 100,
        nowIndicator: true, //周/日视图中显示今天当前时间点(以红线标记),默认false不显示
        weekMode: "fixed", //在月视图里显示周的模式,因为每月周数可能不同,所以月视图高度不一定。fixed:固定显示6周高,日历高度保持不变liquid:不固定周数,高度随周数变化variable:不固定周数,但高度固定
        weekNumbers: true, //是否在日历中显示周次(一年中的第几周),如果设置为true,则会在月视图的左侧、周视图和日视图的左上角显示周数。
        weekText: "周",
        customButtons: {
          //自定义按钮
          getToday: {
            text: "今天",
            click: this.getToday,
          },
          getNext: {
            text: ">",
            click: this.getNext,
          },
          getPrev: {
            text: "<",
            click: this.getPrev,
          },
          getPrevYear: {
            text: "<<",
            click: this.getPrevYear,
          },
          getNextYear: {
            text: ">>",
            click: this.getNextYear,
          },
          customButton: {
            text: "今日标记全部已完成",
            click: this.customButton,
          },
          customButton1: {
            text: "显示未完成",
            click: this.customButton,
          },
        },
        // 日历头部按钮,即Fullcalendar表头显示区域
        // headerToolbar: {
        //   left: "getPrevYear,getPrev,getToday,getNext,getNextYear", //"getPrevYear,getPrev,getToday,getNext,getNextYear customButton,customButton1",
        //   center: "title",
        //   right:
        //     "customButton customButton1 dayGridMonth,timeGridWeek,timeGridDay,listMonth", //dayGridWeek,listMonth
        // },
        headerToolbar: false, // Fullcalendar表头显示区域不显示,显示自己自定义的html头部
        // 使用内置按钮的显示文本
        buttonText: {
          today: "今天",
          month: "月",
          week: "周",
          day: "日",
          list: "日程",
        },
        // 设置日历显示事件时间头
        slotLabelFormat: {
          hour: "2-digit",
          minute: "2-digit",
          meridiem: false,
          hour12: false, // 设置时间为24小时
        },
        // 视图的一些基本设置
        views: {
          // 月视图阳历转农历
          dayGridMonth: {
            height: 500,
            displayEventTime: true, //是否显示时间
            dayMaxEventRows: 4, // adjust to 6 only for timeGridWeek/timeGridDay
            // titleFormat: { year: "numeric", month: "2-digit", day: "2-digit" }, //控制日历显示的标题
            // moreLinkContent: "+ 更多", //可放在这里单独对每个视图控制显示更多的文字
            moreLinkClick: "popover",
            eventTimeFormat: {
              hour: "numeric",
              minute: "2-digit",
              meridiem: false,
            },
            dayPopoverFormat: {
              month: "long",
              day: "numeric",
              year: "numeric",
            },
            // 显示农历
            // dayCellContent(item) {
            //   let _date = new Date(item.date).toLocaleDateString().split("/");
            //   let _dateF = calendar.solarToLunar(_date[0], _date[1], _date[2]);
            //   // 以二十四节气覆盖农历日期
            //   if (calendar.getLunar24Days(_date[0], _date[1], _date[2])) {
            //     _dateF.dayStr = calendar.getLunar24Days(
            //       _date[0],
            //       _date[1],
            //       _date[2]
            //     );
            //   }
            //   return { html: `<p>${item.dayNumberText}(${_dateF.dayStr})</p>` };
            // },
          },
          timeGridWeek: {},
          timeGridDay: {},
          listMonth: {},
        },
        // 设置过往时间无法点击
        // selectAllow: function (clickInfo) {
        //   if (clickInfo.end < new Date()) {
        //     return false;
        //   }
        //   return true;
        // },
        weekends: true, //是否显示周末,设为false则不显示周六和周日
        selectable: true, //是否可以选中日历格
        editable: false, //是否可以进行(拖动、缩放)修改
        navLinks: true, //天链接
        select: this.selectDate, //选中日历格事件
        eventClick: this.handleEventClick, //选中备忘录事件
        eventsSet: this.handleEvents,
        events: this.getCalendarList, //获取数据源
        eventMouseEnter: this.eventMouseEnter, //鼠标悬浮事件
        slotEventOverlap: true, //相同时间段的多个日程视觉上是否允许重叠,默认true允许
        eventResize: this.onEventResize, // 事件时间区间调整
        eventDrop: this.onEventResize, // 事件Drag-Drop事件
        eventMouseLeave: this.eventMouseLeave, // 鼠标移出事件发生的事件
      },
    };
  },
}
  </script>

         在这里,鼠标在事件上面经过时,会显示一个弹出窗,如下图。可见,弹出框有:是否已完成,开始时间,图列说明(可以是图片、GIF等),文字说明、链接或是附件。以上的这些都是用el-popover实现,用了v-slot:eventContent="arg",将日历的数据进行处理。图片的显示需要修改源码才能显示,不然有bug显示不出来,修改的源码见此文章第3节内容

 2.2 Fullcalendar日历自定义头部

        在calendarOptions设置里,修改headerToolbar,设置为false。

         然后写好自己的html代码,并调整好css样式。

         绑定自定义按钮的函数功能,主要是利用了calendarApi自带的函数功能,包括视图切换、月日视图切换、往前和往后功能等,当然搜索功能是自己定义的。

  2.3 搜索功能

        这里是onSearch函数功能,主要是在前端对events的过滤,然后再设置视图为list视图,注意这个视图在日历头部的功能区是没有的,但是是Fullcalendar内置的。当搜索框为空或者清空搜索字符后,需要重新请求后端数据。 

3、Fullcalendar源码修改

3.1 修改源码main.js的地址:

 3.2 添加的show_pic函数/方法:

    CalendarApi.prototype.show_pic = function (arg) {
        var state = this.getCurrentData();
        this.unselect();
        // 出现图片的关键
        this.dispatch({
            type: 'CHANGE_DATE',
            dateMarker: state.dateEnv.createMarker(arg.view.currentStart),
        });
    };

4、Vue源码

<template>
  <!-- el-mian是个人右侧容器的设置组件 -->
  <el-main>
    <div>
      <!-- 日历头部div -->
      <div class="fc-toolbar" style="display: flex; margin-bottom: 2px">
        <!-- 日历头部左侧显示区域 -->
        <div class="fc-left" style="flex: 1; justify-content: flex-start">
          <div style="vertical-align: middle">
            <el-input
              placeholder="请输入查询内容"
              v-model="search_input"
              clearable
              class="input"
              size="medium"
              @keyup.enter.native="onSearch"
              @clear="getToday()"
            >
              <i slot="suffix" class="el-icon-search" @click="onSearch"></i>
            </el-input>
          </div>
        </div>
        <!-- 日历头部中间显示区域 -->
        <div
          class="fc-center"
          style="display: flex; flex: 3; justify-content: center"
        >
          <el-button-group>
            <el-button
              icon="el-icon-d-arrow-left"
              @click="getPrevYear"
              class="fc_btns"
            ></el-button>
            <el-button
              icon="el-icon-arrow-left"
              @click="getPrev"
              class="fc_btns"
            ></el-button>
          </el-button-group>
          <h2 class="title">
            {{ title }}
          </h2>
          <el-button-group>
            <el-button
              icon="el-icon-arrow-right"
              @click="getNext"
              class="fc_btns"
            ></el-button>
            <el-button
              icon="el-icon-d-arrow-right"
              @click="getNextYear"
              class="fc_btns"
            ></el-button>
          </el-button-group>
        </div>
        <!-- 显示图标注释栏 -->
        <!-- <div class="tips" style="display: flex">
          <div
            style="
              height: 14px;
              width: 14px;
              background: green;
              text-align: center;
              position: relative;
              top: 27%;
            "
          ></div>
          <span class="tip-content">已完成</span>
          <div
            style="
              height: 14px;
              width: 14px;
              background: #fe9b02;
              text-align: center;
              position: relative;
              top: 27%;
            "
          ></div>
          <span class="tip-content">未开始</span>
        </div> -->
        <!-- 日历头部右侧显示区域 -->
        <div class="fc-right">
          <el-button-group>
            <el-button
              @click="today"
              type="success"
              plain
              size="medium"
              class="fc_btns"
              >今天</el-button
            >
            <el-button
              @click="month"
              type="primary"
              plain
              size="medium"
              class="fc_btns"
              >月</el-button
            >
            <el-button
              @click="week"
              type="primary"
              plain
              size="medium"
              class="fc_btns"
              >周</el-button
            >
            <el-button
              @click="day"
              type="primary"
              plain
              size="medium"
              class="fc_btns"
              >日</el-button
            >
            <el-button
              @click="list"
              type="primary"
              plain
              size="medium"
              class="fc_btns"
              >列表</el-button
            >
          </el-button-group>
        </div>
      </div>
    </div>
    <!-- 日历本体 -->
    <el-row>
      <el-col :md="24" :xs="24">
        <div style="margin-top: 0px">
          <FullCalendar
            class="calendar"
            ref="fullCalendar"
            :options="calendarOptions"
            ><template v-slot:eventContent="arg">
              <el-popover
                :append-to-body="true"
                ref="popover1"
                placement="top-start"
                width="220"
                :visible-arrow="true"
                trigger="hover"
                :teleported="false"
                popper-class="popover"
                :open-delay="100"
                @show="showPic(arg)"
                @hide="popoverPicReset(arg)"
              >
                <el-row class="popover_title">
                  <el-col
                    :span="12"
                    :style="{
                      color:
                        arg.event.extendedProps.isDone == false
                          ? 'red'
                          : 'green',
                    }"
                  >
                    <span
                      style="padding-right: 2px"
                      :style="{
                        'border-left':
                          arg.event.extendedProps.isDone == false
                            ? '5px solid red'
                            : '5px solid green',
                      }"
                    ></span
                    >{{
                      arg.event.extendedProps.isDone == false
                        ? "未开始"
                        : "已完成"
                    }}</el-col
                  >
                  <el-col
                    :span="12"
                    style="
                      display: flex;
                      flex-direction: row-reverse;
                      font-size: 14;
                      color: #000;
                    "
                    >{{
                      arg.event.allDay == true
                        ? "全天"
                        : formatTimer(arg.event.start)
                    }}</el-col
                  >
                </el-row>
                <el-row>
                  <el-col :span="24" style="text-align: center">
                    <el-image
                      v-if="popoverimg.length != 0"
                      :src="popoverimg[0]"
                      @click="PreviewPic(popoverimg)"
                      fit="fill"
                      class="popoverShowImg"
                      ><div slot="placeholder" class="image-slot">
                        加载中<span class="dot">...</span>
                      </div></el-image
                    >
                    <div class="block"></div>
                  </el-col>
                </el-row>
                <el-row class="popover_content">
                  <el-col :span="24" style="max-height: 150px; overflow: auto">
                    <span class="click">{{ arg.event.title }}</span>
                  </el-col>
                  <el-col :span="24">
                    <el-link
                      v-if="
                        arg.event.extendedProps.address != null &&
                        arg.event.extendedProps.address != ''
                      "
                      :href="undefined"
                      :underline="false"
                      @click="fileDownload(arg.event.extendedProps.address)"
                      class="link"
                      >{{
                        arg.event.extendedProps.address == null
                          ? ""
                          : arg.event.extendedProps.address.replace(
                              "D:\\flask\\upload\\",
                              ""
                            )
                      }}</el-link
                    ></el-col
                  >
                </el-row>

                <el-row style="margin-top: 5px"
                  ><el-col style="width: 15%"
                    ><div>
                      <el-button
                        class="hvr-icon-pulse-grow"
                        :popperAppendToBody="false"
                        size="mini"
                        icon="el-icon-edit hvr-icon"
                        type="primary"
                        circle
                        @click="handleEventClick(arg)"
                      >
                      </el-button>
                    </div>
                  </el-col>
                  <el-col style="width: 15%"
                    ><el-button
                      class="hvr-icon-bounce"
                      size="mini"
                      type="success"
                      icon="el-icon-document-checked hvr-icon"
                      circle
                      @click="onCheckBtnClicked(arg)"
                    >
                    </el-button>
                  </el-col>
                  <el-col style="width: 15%"
                    ><el-popconfirm
                      confirm-button-text="好"
                      cancel-button-text="否"
                      icon="el-icon-info"
                      icon-color="red"
                      title="确定删除这个事项吗?"
                      @confirm="onRemoveBtnClicked(arg)"
                      ><el-button
                        class="hvr-icon-buzz-out"
                        slot="reference"
                        size="mini"
                        type="danger"
                        icon="el-icon-delete hvr-icon"
                        circle
                      >
                      </el-button
                    ></el-popconfirm> </el-col
                ></el-row>
                <div slot="reference">
                  <span class="tree_span_text">{{ arg.timeText }}</span>
                  <span>{{ arg.event.title }}</span>
                </div>
              </el-popover>
            </template>
          </FullCalendar>
        </div>
        <!-- 事件添加或修改对话框 -->
        <el-dialog
          :visible.sync="dialogVisible"
          :popperAppendToBody="false"
          @close="cancel"
          v-dialogDrag
          :close-on-click-modal="false"
          class="calendar_matters"
        >
          <div slot="title" class="header-title" :style="{ color: 'black' }">
            <i class="el-icon-edit"></i><span> &nbsp;事件</span>
          </div>
          <el-form ref="form" :model="form" label-width="80px">
            <el-row>
              <el-col :span="12" :xs="24">
                <el-form-item label="事件时间">
                  <div class="dateRange">
                    <el-date-picker
                      v-model="dateRange"
                      type="datetimerange"
                      range-separator="至"
                      start-placeholder="开始日期"
                      end-placeholder="结束日期"
                    >
                    </el-date-picker>
                  </div>
                </el-form-item>
              </el-col>
              <el-col :span="12"> </el-col>
            </el-row>
            <el-row>
              <el-col :span="24"
                ><el-form-item label="具体事项">
                  <el-input
                    v-model="form.remark"
                    type="textarea"
                    class="calendar_details"
                  ></el-input> </el-form-item
              ></el-col>
            </el-row>
            <el-row>
              <el-col :span="12" :xs="24"
                ><el-form-item label="提醒类别" prop="deptId">
                  <treeselect
                    v-model="form.category"
                    :options="Options"
                    :props="defaultProps"
                    :show-count="true"
                    placeholder="请选择类型"
                    @select="categorySelected"
                  /> </el-form-item
              ></el-col>
              <el-col :span="12" :xs="24"
                ><el-form-item
                  v-if="form.userId == undefined"
                  label="记录人"
                  prop="userName"
                >
                  <el-input
                    v-model="form.userName"
                    maxlength="30"
                    disabled
                    class="userName"
                  /> </el-form-item
              ></el-col>
            </el-row>

            <el-row>
              <el-col :span="12"> </el-col>
              <el-col :span="12" :xs="24">
                <el-form-item label="状态">
                  <el-radio-group v-model="form.isDone">
                    <el-radio label="0">已完成</el-radio>
                    <el-radio label="1">未确认</el-radio>
                  </el-radio-group>
                </el-form-item>
              </el-col>
            </el-row>
            <el-row>
              <el-col :span="18" :xs="24">
                <el-form-item label="附件" class="attachment" prop="address">
                  <el-upload
                    action="#"
                    :show-file-list="false"
                    :auto-upload="false"
                    :on-change="address_beforeupload"
                  >
                    <div>
                      <el-button
                        type="primary"
                        icon="el-icon-folder-opened"
                      ></el-button>
                    </div>
                  </el-upload>
                  <el-input
                    v-model="form.address"
                    clearable
                  ></el-input></el-form-item
              ></el-col>
            </el-row>
            <el-row>
              <el-col :span="24" :xs="24">
                <el-form-item label="相关图片" prop="address">
                  <el-upload
                    ref="uploadFile"
                    class="upload-demo"
                    action="#"
                    :auto-upload="false"
                    :show-file-list="true"
                    :on-change="beforeupload"
                    list-type="picture-card"
                    :file-list="filelist"
                    multiple
                  >
                    <i slot="default" class="el-icon-plus"></i>
                    <div slot="file" slot-scope="{ file }">
                      <img
                        class="el-upload-list__item-thumbnail"
                        :src="file.url"
                        alt=""
                      />
                      <span class="el-upload-list__item-actions">
                        <span
                          class="el-upload-list__item-preview"
                          @click="handlePictureCardPreview(file)"
                        >
                          <i class="el-icon-zoom-in"></i>
                        </span>
                        <span
                          v-if="!disabled"
                          class="el-upload-list__item-delete"
                          @click="handleDownload(file)"
                        >
                          <i class="el-icon-download"></i>
                        </span>
                        <span
                          v-if="!disabled"
                          class="el-upload-list__item-delete"
                          @click="handleRemove(file)"
                        >
                          <i class="el-icon-delete"></i>
                        </span>
                      </span>
                    </div>
                  </el-upload>
                </el-form-item>
              </el-col>
            </el-row>
          </el-form>
          <div slot="footer" class="dialog-footer">
            <el-button type="primary" @click="submitForm">确 定</el-button>
            <el-button @click="cancel">取 消</el-button>
          </div>
        </el-dialog>
        <!-- 图片预览对话框 -->
        <el-image-viewer
          v-if="img_dialogVisible"
          :initial-index="0"
          :on-close="onClose"
          :url-list="dialogImageUrl"
          style="z-index: 3000"
        ></el-image-viewer>
      </el-col>
    </el-row>
  </el-main>
</template>

<script>
// import { getCalendarList } from "@/api/calendar.js";
import FullCalendar from "@fullcalendar/vue";
import dayGridPlugin from "@fullcalendar/daygrid";
import timeGridPlugin from "@fullcalendar/timegrid";
import interactionPlugin from "@fullcalendar/interaction";
import listPlugin from "@fullcalendar/list";
// import tippy from "tippy.js";
// import "../../assets/tippy.css";
// import calendar from "../../utils/calendar.js";
// import { INITIAL_EVENTS, createEventId } from "./event-utils";
import { auto } from "@popperjs/core";
import "../../directives.js"; // v-dialogDrag: 弹窗可拖拽属性
import Treeselect from "@riophae/vue-treeselect"; // Treeselect插件
import "@riophae/vue-treeselect/dist/vue-treeselect.css"; // 若依css设置
export default {
  // 注册局部组件
  components: {
    FullCalendar,
    Treeselect,
    "el-image-viewer": () =>
      import("element-ui/packages/image/src/image-viewer"),
  },
  data() {
    return {
      // 搜索框输入的文本
      search_input: "",

      // treeselect插件默认配置
      defaultProps: {
        children: "children",
        label: "label",
      },
      // 提醒类别设置
      Options: [
        {
          id: "工作类别",
          pid: 0,
          label: "工作类别",
          children: [],
        },
        {
          id: "生活类别",
          pid: 0,
          label: "生活类别",
          children: [],
        },
        {
          id: "其他类别",
          pid: 0,
          label: "其他类别",
          children: [],
        },
        {
          id: "",
          pid: 0,
          label: "无",
          children: [],
        },
      ],

      // 表单是否显示
      dialogVisible: false,
      // 表单标题栏
      title: "事件",
      // 表单当前编辑模式设置,add或amend
      form_edited_state: "",
      // 表单内容设置项
      form: {
        remark: undefined,
        isDone: undefined,
        img: "",
        address: "",
      },
      // 表单日期设置
      dateRange: [],
      // 图片预览的操作按钮是否展示
      disabled: false,
      // 图片是否显示
      img_dialogVisible: false,
      // 预览图片地址
      dialogImageUrl: "",
      // el-popover弹出框标题内容
      popover_title: "事项",
      // el-popover图片地址存放
      popoverimg: [],
      // 附件地址存放
      filelist: [],

      // ------------------
      // Fullcalendar版本@5.11.3日历控件设置项,官网文档地址:https://fullcalendar.io/
      calendarOptions: {
        // visibleRange: { start: "2021-09-01", end: "2022-10-01" }, // 可视化区间
        // validRange: { start: "2021-09-01", end: "2021-09-01" }, // 可展示区间
        // 引入的插件,比如fullcalendar/daygrid,fullcalendar/timegrid引入后才可显示月,周,日
        plugins: [dayGridPlugin, interactionPlugin, timeGridPlugin, listPlugin],
        initialView: "dayGridMonth", // 默认为那个视图(月:dayGridMonth,周:timeGridWeek,日:timeGridDay)
        firstDay: 1, // 设置一周中显示的第一天是哪天,周日是0,周一是1,类推
        locale: "zh-cn", // 切换语言,当前为中文
        allDaySlot: true, // 不显示all-day
        businessHours: true, //
        handleWindowResize: true, // 是否随浏览器窗口大小变化而自动变化。
        allDayText: "全天", // 设置all-Day显示的文字,不设置的话默认显示"all-Day"
        themeSystem: "bootstrap", // 主题色(本地测试未能生效)
        // loading: this.loadingEvent, // 视图数据加载中、加载完成触发(用于配合显示/隐藏加载指示器。)
        // selectAllow: this.selectAllow, //编程控制用户可以选择的地方,返回true则表示可选择,false表示不可选择
        // eventMouseEnter: this.eventMouseEnter, // 鼠标滑过
        allowContextMenu: false,
        editable: true, // 是否可以进行(拖动、缩放)修改
        eventStartEditable: true, // Event日程开始时间可以改变,默认true,如果是false其实就是指日程块不能随意拖动,只能上下拉伸改变他的endTime
        eventDurationEditable: true, // Event日程的开始结束时间距离是否可以改变,默认true,如果是false则表示开始结束时间范围不能拉伸,只能拖拽
        selectable: true, // 是否可以选中日历格
        selectMirror: true,
        selectMinDistance: 0, // 选中日历格的最小距离
        // eventLimit: true, //数据条数太多时,限制各自里显示的数据条数(多余的以“+2more”格式显示),默认false不限制,支持输入数字设定固定的显示条数
        moreLinkContent: "+ 更多", //当一块区域内容太多以"+2 more"格式显示时,这个more的名称自定义
        // dayPopoverFormat: "YYYY-M-d",
        dayMaxEventRows: true, // 日历显示事件最大条数,for all non-TimeGrid views
        weekends: true, //
        navLinks: true, // 天链接
        selectHelper: false,
        slotEventOverlap: false, // 相同时间段的多个日程视觉上是否允许重叠,默认true允许
        aspectRatio: 1.35, //设置日历单元格宽度与高度的比例。
        expandRows: true,
        height: auto,
        contentHeight: 100,
        nowIndicator: true, //周/日视图中显示今天当前时间点(以红线标记),默认false不显示
        weekMode: "fixed", //在月视图里显示周的模式,因为每月周数可能不同,所以月视图高度不一定。fixed:固定显示6周高,日历高度保持不变liquid:不固定周数,高度随周数变化variable:不固定周数,但高度固定
        weekNumbers: true, //是否在日历中显示周次(一年中的第几周),如果设置为true,则会在月视图的左侧、周视图和日视图的左上角显示周数。
        weekText: "周",
        customButtons: {
          //自定义按钮
          getToday: {
            text: "今天",
            click: this.getToday,
          },
          getNext: {
            text: ">",
            click: this.getNext,
          },
          getPrev: {
            text: "<",
            click: this.getPrev,
          },
          getPrevYear: {
            text: "<<",
            click: this.getPrevYear,
          },
          getNextYear: {
            text: ">>",
            click: this.getNextYear,
          },
          customButton: {
            text: "今日标记全部已完成",
            click: this.customButton,
          },
          customButton1: {
            text: "显示未完成",
            click: this.customButton,
          },
        },
        // 日历头部按钮,即Fullcalendar表头显示区域
        // headerToolbar: {
        //   left: "getPrevYear,getPrev,getToday,getNext,getNextYear", //"getPrevYear,getPrev,getToday,getNext,getNextYear customButton,customButton1",
        //   center: "title",
        //   right:
        //     "customButton customButton1 dayGridMonth,timeGridWeek,timeGridDay,listMonth", //dayGridWeek,listMonth
        // },
        headerToolbar: false, // Fullcalendar表头显示区域不显示,显示自己自定义的html头部
        // 使用内置按钮的显示文本
        buttonText: {
          today: "今天",
          month: "月",
          week: "周",
          day: "日",
          list: "日程",
        },
        // 设置日历显示事件时间头
        slotLabelFormat: {
          hour: "2-digit",
          minute: "2-digit",
          meridiem: false,
          hour12: false, // 设置时间为24小时
        },
        // 视图的一些基本设置
        views: {
          // 月视图阳历转农历
          dayGridMonth: {
            height: 500,
            displayEventTime: true, //是否显示时间
            dayMaxEventRows: 4, // adjust to 6 only for timeGridWeek/timeGridDay
            // titleFormat: { year: "numeric", month: "2-digit", day: "2-digit" }, //控制日历显示的标题
            // moreLinkContent: "+ 更多", //可放在这里单独对每个视图控制显示更多的文字
            moreLinkClick: "popover",
            eventTimeFormat: {
              hour: "numeric",
              minute: "2-digit",
              meridiem: false,
            },
            dayPopoverFormat: {
              month: "long",
              day: "numeric",
              year: "numeric",
            },
            // 显示农历
            // dayCellContent(item) {
            //   let _date = new Date(item.date).toLocaleDateString().split("/");
            //   let _dateF = calendar.solarToLunar(_date[0], _date[1], _date[2]);
            //   // 以二十四节气覆盖农历日期
            //   if (calendar.getLunar24Days(_date[0], _date[1], _date[2])) {
            //     _dateF.dayStr = calendar.getLunar24Days(
            //       _date[0],
            //       _date[1],
            //       _date[2]
            //     );
            //   }
            //   return { html: `<p>${item.dayNumberText}(${_dateF.dayStr})</p>` };
            // },
          },
          timeGridWeek: {},
          timeGridDay: {},
          listMonth: {},
        },
        // 设置过往时间无法点击
        // selectAllow: function (clickInfo) {
        //   if (clickInfo.end < new Date()) {
        //     return false;
        //   }
        //   return true;
        // },
        weekends: true, //是否显示周末,设为false则不显示周六和周日
        selectable: true, //是否可以选中日历格
        editable: false, //是否可以进行(拖动、缩放)修改
        navLinks: true, //天链接
        select: this.selectDate, //选中日历格事件
        eventClick: this.handleEventClick, //选中备忘录事件
        eventsSet: this.handleEvents,
        events: this.getCalendarList, //获取数据源
        eventMouseEnter: this.eventMouseEnter, //鼠标悬浮事件
        slotEventOverlap: true, //相同时间段的多个日程视觉上是否允许重叠,默认true允许
        eventResize: this.onEventResize, // 事件时间区间调整
        eventDrop: this.onEventResize, // 事件Drag-Drop事件
        eventMouseLeave: this.eventMouseLeave, // 鼠标移出事件发生的事件
      },
    };
  },
  mounted() {
    this.calendarApi = this.$refs.fullCalendar.getApi();
    this.title = this.calendarApi.view.title;
  },
  watch: {
    search_input: {
      handler: function (newData, oldData) {
        if (newData == "") {
          this.month();
          this.getToday();
        }
      },
      deep: true,
    },
  },
  methods: {
    // 将当前时间移至今日事件
    today() {
      this.getToday();
    },
    // 月视图
    month() {
      this.calendarApi.changeView("dayGridMonth");
      this.title = this.calendarApi.view.title;
    },
    // 周视图
    week() {
      this.calendarApi.changeView("timeGridWeek");
      this.title = this.calendarApi.view.title;
    },
    // 日视图
    day() {
      this.calendarApi.changeView("timeGridDay");
      this.handleTime(this.calendarApi.currentData.dateProfile.activeRange);
      this.title = this.calendarApi.view.title;
    },
    // 列表视图
    list() {
      this.calendarApi.changeView("listMonth");
      this.title = this.calendarApi.view.title;
    },
    // 鼠标划过,使用tippy插件显示tooltip
    eventMouseEnter(info) {
      // 非周列表的情况下显示悬浮提示;
      // if (info.view.type != "listWeek") {
      //   tippy(info.el, {
      //     content: info.event.title,
      //     placement: "top-start",
      //   });
      // }
    },
    // 鼠标离开
    eventMouseLeave(arg) {
      // console.log("mouseleave");
      // arg.jsEvent.preventDefault();
    },
    // 事项调整时间区间事件
    onEventResize(arg) {
      let newTimeStart = this.dateFormat(
        "YYYY-mm-dd HH:MM:SS",
        arg.event.startStr
      );
      let newTimeEnd = this.dateFormat("YYYY-mm-dd HH:MM:SS", arg.event.endStr);
      this.get("/calendar/updateTime", {
        id: arg.event.id,
        Start: newTimeStart,
        End: newTimeEnd,
      }).then((res) => {
        this.$message.success(res.data.info);
      });
      // 必须加这句,不然切换视图会有显示事件数目的bug
      let calendarApi = arg.view.calendar;
      calendarApi.today();
    },
    // el-popover图片点击预览放大事件
    PreviewPic(arg) {
      this.dialogImageUrl = arg;
      this.img_dialogVisible = true;
      this.stopMove();
    },
    // el-popover图片点击预览后关闭事件
    onClose() {
      this.img_dialogVisible = false;
      this.move();
    },
    // el-popover隐藏时触发,将图片地址修改为空
    async popoverPicReset(arg) {
      this.popoverimg = await [];
      let calendarApi = arg.view.calendar;
      calendarApi.show_pic(arg);
    },
    // el-popover显示图片功能
    async showPic(arg) {
      let calendarApi = arg.view.calendar;
      this.popoverimg = [];

      await this.post("/get_img_url", arg.event.extendedProps.img, "blob").then(
        (res) => {
          // console.log(res.data.imgs);
          res.data.imgs.forEach((item, index) => {
            const img = "data:image/png;base64," + item;
            this.file = this.base64ImgtoFile(img); // 得到File对象
            const url =
              window.webkitURL.createObjectURL(this.file) ||
              window.URL.createObjectURL(this.file);
            this.popoverimg.push(url);
          });
        }
      );
      calendarApi.show_pic(arg);
    },
    // 提醒类别选择
    categorySelected(node) {
      this.form.categoryName = node.label;
    },
    // 自定义按钮
    customButton() {
      console.log("点击了自定义按钮");
    },
    // 今天
    getToday() {
      let calendarApi = this.$refs.fullCalendar.getApi();
      calendarApi.today();
      this.handleTime(calendarApi.currentData.dateProfile.activeRange);
      this.title = this.calendarApi.view.title;
      this.search_input = "";
    },
    // 上一年
    getPrevYear() {
      let calendarApi = this.$refs.fullCalendar.getApi();
      calendarApi.prevYear();
      // this.handleTime(calendarApi.currentData.dateProfile.activeRange);
      this.title = this.calendarApi.view.title;
    },
    // 下一年
    getNextYear() {
      let calendarApi = this.$refs.fullCalendar.getApi();
      calendarApi.nextYear();
      // this.handleTime(calendarApi.currentData.dateProfile.activeRange);
      this.title = this.calendarApi.view.title;
    },
    // 上一月
    getPrev() {
      let calendarApi = this.$refs.fullCalendar.getApi();
      calendarApi.prev();
      // this.handleTime(calendarApi.currentData.dateProfile.activeRange);
      this.title = this.calendarApi.view.title;
    },
    // 下一月
    getNext() {
      let calendarApi = this.$refs.fullCalendar.getApi();
      calendarApi.next();
      // this.handleTime(calendarApi.currentData.dateProfile.activeRange);
      this.title = this.calendarApi.view.title;
    },
    // 处理时间格式
    handleTime(activeRange) {
      let result = {
        startStr: activeRange.start,
        endStr: activeRange.end,
      };
      this.getCalendarList();
    },
    // 获取列表信息
    getCalendarList(result) {
      // 以当前时间插入数据
      let _this = this;
      // 注意,请求的数据是数据库所有数据即可,不用考虑当前显示的时间范围,Fullcalendar会自动只显示当前日期范围的事件
      _this
        .get("/calendar/getCalendarList", "")
        .then((res) => {
          _this.calendarOptions.events = [];
          res.data.data.forEach((item) => {
            var data = {
              id: item[0],
              title: item[1],
              start: item[2],
              end: item[3],
              allDay: item[4],
              className: item[5] == true ? "borderGreen" : "borderOrange",
              // 非标准字段:除上述字段外,您还可以在每个事件对象中包含自己的非标准字段。FullCalendar不会修改或删除这些字段。例如,开发人员通常包括描述在回调中使用的字段,如事件呈现挂钩. 任何非标准属性都将移动到extendedProps哈希期间事件解析.
              extendedProps: {
                isDone: item[5],
                img: item[6],
                address: item[7],
                type: item[8],
              },
              others: "该字段值会被自动归类到extendedProps里面",
              backgroundColor:
                (item[4] == true ? "all_Day" : "other") != "all_Day"
                  ? item[5] == true
                    ? "#c2fccd"
                    : "#FFECDC"
                  : "#66b1ff",
              editable: true, // 是否可以进行(拖动、缩放)修改
            };
            _this.calendarOptions.events.push(data);
          });
        })
        .catch((error) => {
          this.$message.error(error);
        });
    },

    // 选择日期,填写事件
    selectDate: function (arg) {
      let startTime = this.dateFormat("YYYY-mm-dd HH:MM", arg.start);
      let endTime = this.dateFormat("YYYY-mm-dd HH:MM", arg.end);
      this.dialogVisible = true;
      this.form_edited_state = "add";
      this.dateRange = [startTime, endTime]; // 设置当前记录事件的选择的时间段
      let info = JSON.parse(localStorage.getItem("userInfo")); // 获取当前记录人信息
      this.form.userName = info.username; // 设置记录人
      this.form.isDone = "1"; // 默认设置为事件未完成状态
      this.form.isAllDay = arg.allDay;
      let calendarApi = arg.view.calendar;
      calendarApi.unselect(); // 清除当前日期选择
    },
    // 表单确定按钮,提交事件
    submitForm() {
      let calendarApi = this.$refs.fullCalendar.getApi();
      var startTime = this.dateFormat("YYYY-mm-dd HH:MM:SS", this.dateRange[0]);
      var endTime = this.dateFormat("YYYY-mm-dd HH:MM:SS", this.dateRange[1]);
      if (this.form_edited_state == "add") {
        // 添加事件的后端数据请求
        this.get(
          "/calendar/eventRecord",
          {
            isAllDay: this.form.isAllDay,
            dateRange: JSON.stringify([startTime, endTime]),
            remark: this.form.remark,
            type: this.form.type == undefined ? "" : this.form.type,
            isDone: this.form.isDone == "1" ? false : true,
            userName: this.form.userName,
            address: this.form.address == undefined ? "" : this.form.address,
            img: this.form.img == null ? "" : this.form.img,
          },
          ""
        ).then((res) => {
          this.$message.success("事件添加成功!");
          this.handleTime(calendarApi.currentData.dateProfile.activeRange);
        });
      } else if (this.form_edited_state == "amend") {
        // 修改事件的后端数据请求
        this.get(
          "/calendar/submit",
          {
            id: this.form.id,
            dateRange: JSON.stringify([startTime, endTime]),
            remark: this.form.remark,
            type: this.form.type == undefined ? "" : this.form.type,
            isDone: this.form.isDone == "1" ? false : true,
            userName: this.form.userName,
            address: this.form.address == undefined ? "" : this.form.address,
            img: this.form.img == null ? "" : this.form.img,
            // type: this.form.category,
          },
          ""
        ).then((res) => {
          this.$message.success("修改事项成功!");
          if (this.search_input == "") {
            this.handleTime(calendarApi.currentData.dateProfile.activeRange);
          }
        });
      }
      this.dialogVisible = false;
    },
    // 表单取消按钮
    cancel() {
      this.form = {
        id: "",
        dateRange: "",
        remark: "",
        type: "",
        isDone: false,
        userName: "",
        address: "",
        img: "",
      };
      this.filelist = [];
      this.dialogVisible = false;
      this.form_edited_state = "";
    },
    // 点击事项事件
    handleEventClick(clickInfo) {
      this.dialogVisible = true;
      this.form_edited_state = "amend"; //修改状态
      var startTime = this.dateFormat(
        "YYYY-mm-dd HH:MM:SS",
        clickInfo.event.startStr
      );
      var endTime = this.dateFormat(
        "YYYY-mm-dd HH:MM:SS",
        clickInfo.event.endStr
      );
      // 设置打开对话框各部分的显示值
      this.form.id = clickInfo.event.id;
      this.dateRange = [clickInfo.event.start, clickInfo.event.end];
      this.form.remark = clickInfo.event.title;
      // 获取当前登录用户的名字
      let info = JSON.parse(localStorage.getItem("userInfo"));
      this.form.userName = info.username;
      this.form.isDone =
        clickInfo.event.extendedProps.isDone == true ? "0" : "1";
      this.form.img = clickInfo.event.extendedProps.img;
      this.form.address = clickInfo.event.extendedProps.address;
      this.form.category =
        clickInfo.event.extendedProps.type == undefined
          ? ""
          : clickInfo.event.extendedProps.type;
      // 请求后端图片URL方法
      this.post("/get_img_url", clickInfo.event.extendedProps.img).then(
        (res) => {
          res.data.imgs.forEach((item, index) => {
            const img = "data:image/png;base64," + item;
            this.file = this.base64ImgtoFile(img); // 得到File对象
            const url =
              window.webkitURL.createObjectURL(this.file) ||
              window.URL.createObjectURL(this.file);
            this.filelist.push({
              name: res.data.origin_url[index],
              url: url,
            });
          });
        }
      );
    },
    // 事项标记已完成或改为未完成的事件
    onCheckBtnClicked(arg) {
      this.get(
        "/calendar/checked",
        { id: arg.event.id, status: arg.event.extendedProps.isDone },
        ""
      ).then((res) => {
        this.calendarOptions.events.filter((item) => {
          if (item.id == arg.event.id) {
            item.extendedProps.isDone = !arg.event.extendedProps.isDone;
            arg.event.extendedProps.isDone == true
              ? (item.className = "borderOrange")
              : (item.className = "borderGreen");
            item.backgroundColor =
              (arg.event.allDay == true ? "all_Day" : "other") != "all_Day"
                ? arg.event.extendedProps.isDone == true
                  ? "#FFECDC"
                  : "#c2fccd"
                : "#66b1ff";
          }
          return item;
        });
        this.$message.success("事件状态修改成功!");
      });
    },
    // 事项删除事件
    onRemoveBtnClicked(arg) {
      this.get("/calendar/remove", arg.event.id).then((res) => {
        this.calendarOptions.events = this.calendarOptions.events.filter(
          (item) => {
            return item.id != arg.event.id;
          }
        );
      });
    },
    // 当前事件绑定,此段代码可删掉
    handleEvents(events) {
      this.currentEvents = events;
    },
    // 提交上传文件
    submitFileForm() {
      /* 这里为啥会先发一个Option请求再发Post请求:这是浏览器处理跨域做的逻辑。
      CORS跨域请求会先发option请求,如果server返回access-control-allow-origin头为*或者和当前域名一致的话,
      才会进入第二段的真正请求。不然就会报 cross origin request is forbidden错误。*/
      this.$refs.upload.submit();
    },
    // 对话框图片预览放大
    handlePictureCardPreview(file) {
      this.dialogImageUrl = [file.url];
      this.img_dialogVisible = true;
    },
    // 预览图片下载到本地
    handleDownload(file) {
      // console.log(file, "@@@@");
      this.get("/bbx_img_download", file.name, "blob").then((res) => {
        var blob = new Blob([res.data], {
          type: "application/octet-stream;chartset=UTF-8",
        });
        var url = window.URL.createObjectURL(blob);
        var a = document.createElement("a");
        a.href = url;
        //文件名
        let name = res.config.params.replace("F:\\flask\\upload\\", "");
        a.download = name;
        a.click();
        window.URL.revokeObjectURL(url); // 释放掉blob对象
      });
    },
    // 下载文件
    fileDownload(file) {
      // console.log(this.urlToLink(file));
      // 判断是否为网页
      let bool = this.urlToLink(file);
      if (bool) {
        var b = document.createElement("a");
        b.setAttribute("href", file);
        b.setAttribute("target", "_blank");
        document.body.appendChild(b);
        b.click();
      } else {
        if (file != null || file != "") {
          this.get("/get_file_download", file, "blob").then((res) => {
            var blob = new Blob([res.data], {
              type: "application/octet-stream;chartset=UTF-8",
            });
            var url = window.URL.createObjectURL(blob);
            var a = document.createElement("a");
            a.href = url;
            //文件名
            let name = res.config.params.replace("F:\\flask\\upload\\", "");
            a.download = name;
            a.click();
            window.URL.revokeObjectURL(url); // 释放掉blob对象
          });
        } else {
        }
      }
    },
    // 移除对话框预览图片
    handleRemove(file) {
      let address = this.form.img.split(",");
      address = address.filter((item) => {
        return item != file.name;
      });
      address = address.join(",");
      this.form.img = address;
      const newArray = this.filelist.filter((item, index) => {
        return item.uid != file.uid;
      });
      this.filelist = newArray;
    },
    // 判断字符串是否为网页
    urlToLink(str) {
      var reg = /(http:\/\/|https:\/\/)((\w|=|\?|\.|\/|&|-)+)/g;
      if (reg.test(str)) {
        return true;
      } else {
        return false;
      }
    },
    // 停止页面滚动
    stopMove() {
      const m = (e) => {
        e.preventDefault();
      };
      document.body.style.overflow = "hidden";
      document.addEventListener("touchmove", m, false); // 禁止页面滑动
    },
    // 开启页面滚动
    move() {
      const m = (e) => {
        e.preventDefault();
      };
      document.body.style.overflow = "auto";
      document.removeEventListener("touchmove", m, true);
    },
    // 查询功能
    onSearch() {
      let search_text = this.search_input;
      let curr_Events = this.calendarOptions.events;
      if (search_text != "") {
        this.calendarApi.changeView("list");
        let result = this.searchStr(search_text, curr_Events);
        this.calendarOptions.events = result;
        this.title = "查询结果";
      } else {
        // this.today();
      }
    },
    // 数组中匹配单个字符串的方法,传入数组支持格式[{},{}],
    searchStr(str, arr) {
      let newList = [];
      // 要匹配字符串的首个字符
      let startChar = str.charAt(0);
      // 要匹配字符串的字符长度
      let strLength = str.length;
      for (let i = 0; i < arr.length; i++) {
        // 默认数组arr中对象arr[i]不存在str
        let isExist = false;
        let obj = arr[i];
        for (let key in obj) {
          if (typeof obj[key] === "function") {
            obj[key]();
          } else {
            let keyValue = "";
            // 获取arr[i][key]的值
            if (obj[key] !== null && typeof obj[key] === "string") {
              keyValue = obj[key];
            } else if (obj[key] !== null && typeof obj[key] !== "string") {
              keyValue = JSON.stringify(obj[key]);
            }
            // arr[i][key]中的各个位置的字符与str的0位置字符startChar对比如果相等,
            // 在arr[i][key]中从j位置截取与str长度相同的字符,对比是否相等
            for (let j = 0; j < keyValue.length; j++) {
              // 把原有数据转化为小写,输入数据也转化为纯小写,实现模糊匹配,如区分大小写,可删除toLowerCase()
              if (
                keyValue.charAt(j).toLowerCase() === startChar.toLowerCase()
              ) {
                if (
                  keyValue
                    .substring(j)
                    .substring(0, strLength)
                    .toLowerCase() === str.toLowerCase()
                ) {
                  // 模糊匹配到的字符存在表示arr[i]中存在str
                  isExist = true;
                  break;
                }
              }
            }
          }
        }
        // 当arr[i]中存在str时,把arr[i]放入一个新数组
        if (isExist === true) {
          newList.push(obj);
        }
      }
      // 最后返回这个新数组
      return newList;
    },
    // 格式化时间 fmt是所需格式化的格式,如"YYYY-mm-dd HH:MM:SS",date是所需格式化的日期
    dateFormat(fmt, date) {
      let ret = "";
      date = new Date(date);
      const opt = {
        "Y+": date.getFullYear().toString(), // 年
        "m+": (date.getMonth() + 1).toString(), // 月
        "d+": date.getDate().toString(), // 日
        "H+": date.getHours().toString(), // 时
        "M+": date.getMinutes().toString(), // 分
        "S+": date.getSeconds().toString(), // 秒
      };
      for (let k in opt) {
        ret = new RegExp("(" + k + ")").exec(fmt);
        if (ret) {
          fmt = fmt.replace(
            ret[1],
            ret[1].length == 1 ? opt[k] : opt[k].padStart(ret[1].length, "0")
          );
        }
      }
      return fmt;
    },
    //日期格式转换
    formatTimer: function (value) {
      let date = new Date(value);
      let y = date.getFullYear();
      let MM = date.getMonth() + 1;
      MM = MM < 10 ? "0" + MM : MM;
      let d = date.getDate();
      d = d < 10 ? "0" + d : d;
      let h = date.getHours();
      h = h < 10 ? "0" + h : h;
      let m = date.getMinutes();
      m = m < 10 ? "0" + m : m;
      let s = date.getSeconds();
      s = s < 10 ? "0" + s : s;
      return h + ":" + m;
    },
    // 获取当前时间戳
    getCurrentTime() {
      let date = new Date();
      let year = date.getFullYear();
      let month = date.getMonth() + 1;
      let day = date.getDate();
      let hour = date.getHours();
      let minute = date.getMinutes();
      let second = date.getSeconds();
      month = month < 10 ? "0" + month : month; // 可注释掉
      day = day < 10 ? "0" + day : day;
      hour = hour < 10 ? "0" + hour : hour; // 可注释掉
      minute = minute < 10 ? "0" + minute : minute;
      second = second < 10 ? "0" + second : second;
      return `${year}/${month}/${day} ${hour}:${minute}:${second}`;
    },
    // 地址上传地址
    address_beforeupload(file, filelist) {
      let formData = new FormData();
      formData.append("file", file.raw);
      this.upload("/get_file_url", formData, "").then((res) => {
        this.form.address = res.data.path;
      });
    },
    /** 图片上传功能 */
    beforeupload(file, filelist) {
      this.filelist = filelist;
      let formData = new FormData();
      formData.append("file", file.raw);
      // 请求数据
      this.upload("/get_file_url", formData, "").then((res) => {
        if (this.form.img == null) {
          this.form.img = res.data.path;
        } else {
          this.form.img = this.form.img + "," + res.data.path;
        }
      });
    },
    // base64字段变成blob二进制数据,关于图片的
    base64ImgtoFile(dataurl, filename = "file") {
      const arr = dataurl.split(",");
      const mime = arr[0].match(/:(.*?);/)[1];
      const suffix = mime.split("/")[1];
      const bstr = atob(arr[1]);
      let n = bstr.length;
      const u8arr = new Uint8Array(n);
      while (n--) {
        u8arr[n] = bstr.charCodeAt(n);
      }
      return new File([u8arr], `${filename}.${suffix}`, {
        type: mime,
      });
    },
  },
};
</script>

<!-- 样式1,本地样式 -->
<style lang="scss" scoped>
.calendar_matters >>> .el-dialog__body {
  height: 450px;
  overflow: auto;
}
.calendar_details >>> .el-textarea__inner {
  font-weight: bold;
  font-family: Arial, Helvetica, sans-serif;
  color: #000;
  height: 120px;
}
.calendar_matters >>> .el-dialog__header {
  border-radius: 5px;
  background-color: #cae1f7;
  align-content: center;
  padding: 15px;
  font-weight: bold;
  border-bottom-style: solid;
  border-bottom-width: 1px;
  border-bottom-color: aliceblue;
}
.fc-daygrid-day-top p {
  font-size: 13px;
  color: #606266;
  margin-right: 10px;
}
.fc .fc-toolbar.fc-header-toolbar {
  margin-bottom: 10px;
}
.el-main {
  padding: 8px 10px 8px 10px;
}
.userName >>> .el-input__inner {
  font-weight: bold;
  color: #000000ab;
}
.attachment >>> .el-form-item__content {
  display: flex;
}
.upload-demo >>> .el-upload-list--picture-card .el-upload-list__item {
  height: 120px;
  width: 120px;
}
.upload-demo >>> .el-upload--picture-card {
  height: 120px;
  width: 120px;
  line-height: 120px;
}
.dateRange >>> .el-range-input {
  font-weight: bold;
  color: #080808;
}
.calendar >>> .fc-header-toolbar {
  margin-bottom: 5px;
}
.calendar >>> .borderGreen {
  border-left: 5px solid #44bb08 !important;
  border-radius: 0;
  border: none;
  white-space: normal;
  overflow: hidden;
  text-overflow: ellipsis;
  span {
    color: #000;
    font-weight: bold;
  }
}
.calendar >>> .borderOrange {
  border-left: 5px solid #fe9b02 !important;
  border-radius: 0;
  border: none;
  white-space: normal;
  overflow: hidden;
  span {
    color: #000;
    font-weight: bold;
  }
}
.calendar >>> .borderOrigin {
  border-radius: 0;
  border: none;
  white-space: normal;
  overflow: hidden;
  text-overflow: ellipsis;
  max-height: 150px;
  span {
    color: #000;
    font-weight: bold;
  }
}
.calendar >>> .fc-event-title {
  font-weight: bold;
  color: #000;
  overflow: hidden;
}
.calendar >>> .fc-event-time {
  font-weight: bold;
  color: #000;
}
.calendar >>> .fc-daygrid-event-dot {
  border: none;
}
.popover {
  .el-popover {
    max-height: 350px;
  }
}
.popover_title {
  font-weight: bold;
  margin-bottom: 3px;
}
.popover_content {
  color: #000;
  font-size: 13px;
}
.popoverShowImg {
  width: auto;
  cursor: pointer;
  >>> .el-image__inner {
    max-height: 200px;
  }
}
.link {
  color: #000;
  font-size: 12px;
  font-weight: bold;
  margin-top: 2px;
  & :hove {
    color: #66b1ff;
  }
}
.fc_btns {
  padding: 10px 12px;
}
.el-icon-search {
  line-height: 2.5;
  margin-right: 8px;
}
.fc-right {
  display: flex;
  flex: 1.5;
  justify-content: flex-end;
}
.tip-content {
  line-height: 2.2;
  margin-right: 4px;
  font-weight: 600;
}
.title {
  margin: 0px 5px;
  line-height: 1.6;
}
.calendar_matters {
  width: 100%;
}
.calendar >>> .el-popover__reference {
  display: grid;
  max-height: 150px;
  overflow: auto;
}
</style>
  <!-- 样式2 -->
  <style>
.el-popover__reference::-webkit-scrollbar {
  /*滚动条整体样式*/
  width: 8px; /*高宽分别对应横竖滚动条的尺寸*/
  height: 1px;
}
.el-popover__reference::-webkit-scrollbar-thumb {
  /*滚动条里面小方块*/
  border-radius: 10px;
  -webkit-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2);
  background: #535353;
}
.el-popover__reference::-webkit-scrollbar-track {
  /*滚动条里面轨道*/
  -webkit-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2);
  border-radius: 2px;
  background: #ededed;
}
.popover .el-popover__title {
  font-weight: bold;
  margin-bottom: 5px;
  border-bottom: solid 1px;
  padding: 2px;
}
@media only screen and (max-width: 767px) {
  .fc-toolbar {
    flex-direction: column;
  }
  .fc-left {
    flex: 1 !important;
  }
  .tips {
    flex: 1 !important;
    justify-content: center;
    margin-bottom: 5px;
  }
  .fc-center {
    flex: 1 !important;
    margin: 5px 0px;
  }
  .fc-right {
    flex: 1;
    justify-content: center !important;
    margin-bottom: 5px;
  }
  .tip-content {
    line-height: 1 !important;
    margin-right: 4px;
    font-weight: 600;
  }
  .title {
    font-size: 20px;
    font-weight: 700;
    line-height: 1.9 !important;
  }
  .fc-list-table {
    word-break: break-all;
    overflow: auto;
  }
  .fc-list-event-title {
    overflow: auto;
  }
  .el-dialog {
    width: 90%;
  }
  .dateRange {
    overflow: auto;
  }
}
</style>

5、 后端Flask源码

from flask import Flask, render_template,\
    request, jsonify, make_response, Response, send_file,session,send_from_directory
from flask_cors import CORS
import datetime
import json
import pypyodbc
from collections import deque
import os
import base64
import hashlib
import xlwt
import openpyxl
import xlrd

app = Flask(__name__,
            static_folder='./templates/static',  # 设置静态文件夹目录
            template_folder="./templates")  # 设置vue编译输出目录dist文件夹,为Flask模板文件目录
# 解决后端跨域问题,不然会在前端网页控制台显示“ccess to XMLHttpRequest at 'http://localhost:8080/api/login' from origin 'null' has been blocked”
CORS(app, supports_credentials=True)
session = {}

@app.route('/')
def index():
    return render_template('index.html',name='index') #使用模板插件,引入index.html。此处会自动Flask模板文件目录寻找index.html文件。

@app.route('/calendar/eventRecord', methods=["GET", "POST"])
def calendar_evnetRecord():
    params = request.values
    date = json.loads(params["dateRange"])
    startTime = datetime.datetime.strptime(date[0],"%Y-%m-%d %H:%M:%S")
    endTime = datetime.datetime.strptime(date[1],"%Y-%m-%d %H:%M:%S")
    # print(params)
    conn = get_conn()
    cur = conn.cursor()
    sql = "insert into 记事本(allDay, startStr,endStr, title, type, isDone, userName,img,address) values(%s, '%s','%s','%s','%s',%s,'%s','%s','%s')" \
          %(params["isAllDay"],startTime,endTime,params["remark"],params["type"],params["isDone"],params["userName"],params["img"],params["address"])
    cur.execute(sql)
    cur.commit()
    cur.close()
    conn.close()
    return jsonify({"code":200})

@app.route('/calendar/getCalendarList', methods=["GET", "POST"])
def calendar_getCalendarList():
    params = request.values
    conn = get_conn()
    cur = conn.cursor()
    sql = "select ID,title,Format(startStr, 'yyyy-MM-dd HH:mm:ss'),Format(endStr, 'yyyy-MM-dd HH:mm:ss'),allDay,isDone,img,address,type " \
          "from 记事本 "
    cur.execute(sql)
    data = cur.fetchall()
    cur.close()
    conn.close()
    return jsonify({"code":200,"data":data})

@app.route('/calendar/submit', methods=["GET", "POST"])
def calendar_submit():
    params = request.values
    date = json.loads(params["dateRange"])
    startTime = datetime.datetime.strptime(date[0],"%Y-%m-%d %H:%M:%S")
    endTime = datetime.datetime.strptime(date[1],"%Y-%m-%d %H:%M:%S")
    conn = get_conn()
    cur = conn.cursor()
    sql="update 记事本 set startStr='%s',endStr='%s',title='%s',type='%s',isDone=%s,userName='%s',img='%s',address='%s' where ID=%s"\
        %(startTime,endTime,params["remark"],params["type"],params["isDone"],params["userName"],params["img"],params["address"]
          ,params["id"])
    cur.execute(sql)
    cur.commit()
    cur.close()
    conn.close()
    return jsonify({"code":200})


@app.route('/calendar/checked', methods=["GET", "POST"])
def calendar_checked():
    params = request.values
    status = True if params["status"] == "false" else False
    conn = get_conn()
    cur = conn.cursor()
    sql = "update 记事本 set isDone=%s where ID=%s" %(status,params["id"])
    cur.execute(sql)
    cur.commit()
    cur.close()
    conn.close()
    return jsonify({"code": 200, "info":"状态修改成功!"})

@app.route('/calendar/remove', methods=["GET", "POST"])
def calendar_remove():
    matters_id = request.values.get("0")
    conn = get_conn()
    cur = conn.cursor()
    sql = "delete from 记事本 where ID=%s" % (matters_id)
    try:
        cur.execute(sql)
        cur.commit()
        return jsonify({"code": 200, "info":"删除成功!"})
    except Exception as e:
        return jsonify({"code": 200, "info":"发生错误!错误代码:"+str(e)})
    finally:
        cur.close()
        conn.close()

@app.route('/calendar/updateTime', methods=["GET", "POST"])
def calendar_updateTime():
    params = request.values
    conn = get_conn()
    cur = conn.cursor()
    sql = "update 记事本 set startStr='%s',endStr='%s' where ID=%s" %(params["Start"],params["End"],params["id"])
    cur.execute(sql)
    cur.commit()
    cur.close()
    conn.close()
    return jsonify({"code": 200, "info": "事件时间修改成功!"})


@app.route('/get_file_download', methods=["GET", "POST"])
def get_file_download():
    file_url = request.values.get("0")
    with open(file_url, 'rb') as file_f:
        res = make_response(file_f.read())  # 用flask提供的make_response 方法来自定义自己的response对象
        # res.headers['Content-Type'] = 'image/jpg'  # 设置response对象的请求头属性'Content-Type'为图片格式
    return res


if __name__ == '__main__':
    # 0.0.0.0 表示同一个局域网均可访问,也可以替换成本机地址:通过命令行命令:ipcofig 获取
    app.run(host='0.0.0.0', port='5000', debug=True)

6 、数据库数据(ACCESS)格式

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐