vue el-calendar组件实现自定义日历功能

1,313 阅读4分钟

概述:目前市面上有很多开源的日历控件,但都需要通过npm安装,可根据自身项目需求选择适合自己的方案,本文主要讲诉element官网自身的el-calenda组件,实现自定义日历功能,废话不多说,直接上代码!

需求描述:

  1. 日历禁用掉非当前日期,不可点击,且颜色置灰
  2. 本月和非本月的日期进行颜色上的区分
  3. 如某天日期添加了信息,则展示当前信息,无添加显示待维护字段,默认显示待维护字段
  4. 日历上的日期+信息展示,可根据筛选条件进行联调,如日期控件切换、条件查询等
  5. 日历可点击,根据是否维护过,可弹出新增或编辑弹框,进行数据修改
  6. 日历只显示当前月数据,格式为月-日
  7. 禁用当前日期之前的数据中,如果无添加,则不显示待维护字段

实际效果图展示:

默认展示 image.png

条件查询后展示

image.png

image.png

核心点:

接口只返回当前月维护过的月份, 如当前月为5月,只维护了23 24 25,则接口只会返回这三天的数据,此时要做的就是先获取当前月份所有的日期,然后将接口数据组装进自己获取日期的数组中,后续在根据业务需求对数据进行处理

源码解析

  1. 组件提供了slot插槽,这个是实现日历自定义的核心。 返回两个参数date:单元格代表的日期,data:日期数据对象
<el-calendar v-model="calendarValue">
    <template slot="dateCell" slot-scope="{date, data}">
    </template>
</el-calendar>
  1. 如果要自定义日历上的显示格式,可通过以下方式进行组装,data.day.split('-').slice(1)[0]:获取当前月, data.day.split('-').slice(2).join():获取当前日,组装后,就展示为了年-月,如05-17
 <div class="calendar-day" >{{ data.day.split('-').slice(1)[0] + '-' + data.day.split('-').slice(2).join() }}</div>
  1. 禁用当前之前之前的日期核心是:先通过class动态获取到当月所有的日期,然后和当前日期进行比对,如果小于当前日期,则加is-pervDay类名标识,在页面初始化或切换日历的时候,以is-pervDay类名为基准,进行动态添加DOM节点类名,最后根据此类名,进行样式上的控制
<div :class="findPrevDate(data.day) ? 'is-pervDay' : 'mainContainer'"></div>
created() {
  this.setDisabledDayClass()
},
findPrevDate(date) {
    const currentDate = new Date();
    const year = currentDate.getFullYear();
    // 月份是从 0 开始计数的,因此要加1
    const month = currentDate.getMonth() + 1 < 10 ? "0" + (currentDate.getMonth() + 1) : currentDate.getMonth() + 1 
    const day = currentDate.getDate();
    const currentDay = year + '-' + month + '-' + day
    return date < currentDay
},
setDisabledDayClass() {
    this.$nextTick(() => {
      if (this.calendarStatus) {
        let disabledDayNode  = this.$refs.refCalendar.$el.querySelectorAll('td')
        disabledDayNode.forEach(element => {
          if (element.children[0].children[0].className === 'is-pervDay') {
            element.className.includes('is-pervDay') ? element.className += '' : element.className += ' disabledDays'
          }
        })
      }
    })
    },
 ::v-deep td.disabledDays {
    // 日历不可点击
    pointer-events: none;
    background-color: rgba(0, 0, 0, .3);
    color: #fff;
  }

此时做到这里有个BUG:切换到其他日期后,之前被禁用的日期样式,还是会被继承,没有被清除,根本原因是,切换日期后,日历组件的DOM结构没有进行重排,还是显示之前的DOM结构,解决方案很简单,在el-calendar组件上加上v-if="calendarStatus",再每次切换或者初始化页面时,对DOM元素进行销毁!

<el-calendar v-model="calendarValue" ref="refCalendar" v-if="calendarStatus"></el-calendar>
gitChangeMonth(date) {
    // 清空DOM缓存,用来每次切换日期,更新DOM节点
    this.calendarStatus = false
    setTimeout(() => {
      this.calendarStatus = true
      this.setDisabledDayClass()
    }, 100)
  },
  1. 想对日历上的内容进行自定义,核心则是:通过自己组装当前月的所有日期数据,和日期上的日期进行比对,相等则展示自定义DOM,否则就影藏,maintenanceMonth:为日期筛选条件,ticketDate:为接口比对参数,如果不通过v-if比对方式,数组有多少个对象,则会在日历上渲染多少个DIV,所有需要去除不匹配日期的数据。

获取当前月所有日期方法:

created() {
  this.getCurrnetDays()
},
getCurrnetDays() {
   // 获取标准时间
    const date = new Date();
    // 获取当前月份(实际月份需要加1)
    // 如果已经选择日期,取当前选择日期中的月份,否则取当前月份
    const currentMonth = this.maintenanceMonth.toString().indexOf('-') > -1 ? this.maintenanceMonth.split('-')[1] : date.getMonth() + 1 < 10 ? "0" + (date.getMonth() + 1) : date.getMonth() + 1
    // 获取当前年份
    const currentYear = date.getFullYear();
    // 获取当前月有多少天
    const currentMonthDays = new Date(currentYear, currentMonth, 0).getDate();
    // 当前月份所有日期集合
    const currentMonthArr = [];
    this.currneAlltDay = []
    for (let day = 1; day <= currentMonthDays; day++) {
        // 截至当前日期为止
            let dateItem = currentMonth + "-" + (day < 10 ? '0' + day : day)
            this.currneAlltDay.push({
              ticketDate: dateItem
            })
    }
    this.getCalendarList()
    return currentMonthArr;
  },

此时组装之后的数据结构为:当前月1号-31号所有的日期数据

image.png

将日历上的月-日,和接口的日-月进行比对,如果想等则展示该DOM元素下自定义的内容,否则影藏

<div v-for="item in currneAlltDay: " style="width: 100%;" :class="item.calendarFlagName ? '' : 'default'" v-if="data.day.split('-').slice(1)[0] + '-' + data.day.split('-').slice(2).join() === item.ticketDate">
  <!-- 接口数据的月日 和 日历的月日  进行匹配 -->
  <div class="tickContainer"  @click="getCurrentDay(item, data.day)">
    <div class="tickName">{{item.ticketName}}</div>
    <div class="tickPrice" v-if="item.calendarFlagName">{{item.ticketPrice}}</div>
  </div>
</div>
  1. 最后在对数据进行组装,实现业务上的需求

完整源码:

<template>
  <div class="app-container" v-loading="loading">
    <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" label-width="73px">
      <el-form-item label="维护月份" prop="maintenanceMonth">
        <el-date-picker
          format="yyyy-MM"
          value-format="yyyy-MM"
          @change="gitChangeMonth"
          :clearable="false"
          @clear="getClear"
          v-model="maintenanceMonth"
          type="month"
          placeholder="选择维护月份"
          :picker-options="pickerOptions">
        </el-date-picker>
      </el-form-item>
      <el-form-item label="景区项目" prop="region">
        <el-select filterable v-model="queryParams.itemRegion" placeholder="请选择景区项目" clearable @change="getTicketArr" @clear="getClear('item')">
          <el-option
            v-for="dict in regionList"
            :key="dict.id"
            :label="dict.itemName"
            :value="dict.id"
          />
        </el-select>
      </el-form-item>
      <el-form-item label="景区门票" prop="ticketRegion">
        <el-select filterable value-key="id" v-model="queryParams.ticketRegion" placeholder="请选择景区门票" clearable @change="selectTick" @clear="getClear">
          <el-option
            v-for="dict in ticketList"
            :key="dict.id"
            :label="dict.ticketName"
            :value="dict"
          />
        </el-select>
      </el-form-item>
      <el-form-item>
        <div class="tips">
          <div class="symbol">*</div>
          <div class="desc">请先选择景区项目和景区门票,再进行设置</div>
        </div>
      </el-form-item>
    </el-form>
    <div class="money-init">单位:元</div>
    <el-calendar v-model="calendarValue" ref="refCalendar" v-if="calendarStatus">
      <template slot="dateCell" slot-scope="{date, data}">
        <!--自定义内容-->
        <div :class="findPrevDate(data.day) ? 'is-pervDay' : 'mainContainer'">
          <!-- 在日历中显示当前日 -->
          <div class="calendar-day" >{{ data.day.split('-').slice(1)[0] + '-' + data.day.split('-').slice(2).join() }}</div>
          <div class="customerBox" :class="calendarArrList.length > 0 ? '' : 'disableContainer'">
            <div v-for="item in currneAlltDay" style="width: 100%;" :class="item.calendarFlagName ? '' : 'default'" v-if="data.day.split('-').slice(1)[0] + '-' + data.day.split('-').slice(2).join() === item.ticketDate">
              <!-- 接口数据的月日 和 日历的月日  进行匹配 -->
              <div class="tickContainer"  @click="getCurrentDay(item, data.day)">
                <div class="tickName">{{item.ticketName}}</div>
                <div class="tickPrice" v-if="item.calendarFlagName">{{item.ticketPrice}}</div>
              </div>
            </div>
          </div>
        </div>
      </template>
    </el-calendar>
    <priceDialog ref="priceDialog" @getInit="getInitcalendar"></priceDialog>
  </div>
</template>

<script>
  import priceDialog from "./priceDialog";
  import { getTicketList, itemList, calendarList } from "@/api/scenice/attractions"
  export default {
    components: { priceDialog },
    data() {
      return {
        ticketDate: {},
        selectMonth: '',
        regionList:[],
        loading: false,
        size: 10,
        page: 0,
        calendarStatus: true,
        queryParams: {},
        currneAlltDay: [],
        maintenanceMonth: new Date(),
        ticketList: [],
        pickerOptions: {
	        disabledDate(time) {
	          return time.getTime() < Date.now() - 8.64e7;
	        }
	      },
        currentDate: '',
        calendarArrList: [],
        calendarValue: new Date(),
        defaultYearMonth: new Date(),
        currentNewDate: (new Date().getMonth() + 1 < 10 ? "0" + (new Date().getMonth() + 1) : currentDate.getMonth() + 1) + '-' + new Date().getDate()
      }
    },
    created() {
      this.setDisabledDayClass()
      this.getRegionList()
      this.getCurrnetDays()
    },
    methods: {
      getCurrnetDays() {
       // 获取标准时间
        const date = new Date();
        // 获取当前月份(实际月份需要加1)
        // 如果已经选择日期,取当前选择日期中的月份,否则取当前月份
        const currentMonth = this.maintenanceMonth.toString().indexOf('-') > -1 ? this.maintenanceMonth.split('-')[1] : date.getMonth() + 1 < 10 ? "0" + (date.getMonth() + 1) : date.getMonth() + 1
        // 获取当前年份
        const currentYear = date.getFullYear();
        // 获取当前月有多少天
        const currentMonthDays = new Date(currentYear, currentMonth, 0).getDate();
        // 当前月份所有日期集合
        const currentMonthArr = [];
        this.currneAlltDay = []
        for (let day = 1; day <= currentMonthDays; day++) {
            // 截至当前日期为止
                let dateItem = currentMonth + "-" + (day < 10 ? '0' + day : day)
                this.currneAlltDay.push({
                  ticketDate: dateItem
                })
        }
        this.getCalendarList()
        return currentMonthArr;
      },
      findPrevDate(date) {
        const currentDate = new Date();
        const year = currentDate.getFullYear();
        const month = currentDate.getMonth() + 1 < 10 ? "0" + (currentDate.getMonth() + 1) : currentDate.getMonth() + 1 // 月份是从 0 开始计数的,因此要加1
        const day = currentDate.getDate();
        const currentDay = year + '-' + month + '-' + day
        return date < currentDay
      },
      setDisabledDayClass() {
        this.$nextTick(() => {
          if (this.calendarStatus) {
            let disabledDayNode  = this.$refs.refCalendar.$el.querySelectorAll('td')
            disabledDayNode.forEach(element => {
              if (element.children[0].children[0].className === 'is-pervDay') {
                element.className.includes('is-pervDay') ? element.className += '' : element.className += ' disabledDays'
              }
            })
          }
        })
      },
      // 查询景区项目
      getRegionList() {
        itemList(this.queryParams).then(response => {
          this.regionList = response.data.filter(item => item.priceMode === 1 && item.status === 1)
        })
      },
      // 查询项目下的门票
      getTicketArr(id) {
        if (id) {
          getTicketList(id).then(response => {
            this.ticketList = response.data
          })
        } else {
          this.ticketList = []
          this.calendarArrList = []
          this.queryParams.ticketRegion = ''
          this.ticketDate = {}
        }
        this.getCurrnetDays()
      },
      // 清空月份/项目/门票
      getClear(type) {
        if (type == 'item') {
          this.ticketList = []
        }
        this.ticketDate = {}
        this.queryParams.ticketRegion = ''
      },
      selectTick(data) {
        this.ticketDate = data
        this.getCurrnetDays()
        this.$forceUpdate()
      },
      getInitcalendar() {
        this.getCurrnetDays()
      },
      gitChangeMonth(date) {
        // 清空DOM缓存,用来每次切换日期,更新DOM节点
        this.calendarStatus = false
        setTimeout(() => {
          this.calendarStatus = true
          this.setDisabledDayClass()
        }, 100)
        this.selectMonth = date.split('-').join().slice(5)
        this.getCurrnetDays()
        this.calendarValue = date
      },
      getCalendarList(data) {
        this.calendarArrList = []
        if (this.ticketDate.id) {
           this.loading = true;
          calendarList(this.ticketDate.id, this.selectMonth ? this.selectMonth : new Date().getMonth() + 1 ).then(response => {
            this.calendarArrList = response.data
            if (this.calendarArrList.length > 0) {
              this.currneAlltDay.forEach((val) => {
                const ticketDate = val.ticketDate
                const matchingItem = this.calendarArrList.find(item => item.ticketDate.split('-').join('-').slice(5) === ticketDate);
                // 维护过
                if (matchingItem) {
                    val.id = matchingItem.id
                    val.ticketName = matchingItem.ticketName
                    val.ticketPrice = matchingItem.ticketPrice
                    val.createBy = matchingItem.createBy;
                    val.createTime = matchingItem.createTime;
                    val.delFlag = matchingItem.delFlag;
                    val.stock = matchingItem.stock;
                    val.ticketId = matchingItem.ticketId;
                    val.updateBy = matchingItem.updateBy;
                    val.updateTime = matchingItem.updateTime;
                    val.calendarFlagName = true;
                  } else {
                    if (val.ticketDate >= this.currentNewDate) {
                      val.ticketName = '待维护'
                      val.calendarFlagName = false;
                    }
                  }
              })
            } else {
              this.currneAlltDay.forEach((item) => {
                item.ticketName = '待维护'
              })
            }
          this.loading = false;
          });
        } else {
          this.currneAlltDay.forEach((item) => {
            if (item.ticketDate >= this.currentNewDate) {
              item.ticketName = '待维护'
            }
          })
        }
      },
      getCurrentDay(item, date) {
        // 修改
        if (item.id) {
          this.$refs.priceDialog.show(item, date)
        } else {
          // 新增
          if (this.queryParams.ticketRegion) {
            this.$refs.priceDialog.show(this.queryParams, date)
          } else {
            this.$modal.msgWarning('请先选择景区项目,再选择景区门票')
          }
        }
      },
      handleDateChange(date) {
        if (date < new Date()) {
        this.calendarValue = new Date(); // 重置为当前日期
      } else {
        this.calendarValue = date;
      }
      }
    }
  }

</script>

<style lang="scss" scoped>
  ::v-deep tbody {
    font-size: 15px;
    color: #757575;
    border: 3px solid #dfe6ec;
  }
  .symbol {
    font-size: 14px;
    color: #ff4949;
    margin: 4px 4px 0 0;
  }
  ::v-deep .el-calendar-day {
    height: 108px;
    border: 2px solid #dfe6ec;
  }
  ::v-deep .current + .el-calendar-day {
    border: 1px;
  }
  ::v-deep .el-calendar__header {
    display: none;
  }
  ::v-deep .el-calendar__body {
    padding: 0;
  }
  ::v-deep.calendar-day {
    color: #7a7aa9;
  }
  .app-container {
    .tips {
      display: flex;
      margin-bottom: 20px;
      .symbol {
        color: #ff4949;
      }
      .desc {
        color: #ff4949;
      }
    }
    .nameScenic {
      color: #97a8be;
    }
    .el-calendar {
      border-top: 1px solid #dfe6ec;
      .is-pervDay {
        height: 100%;
      }
      .mainContainer {
        height: 100%;
      }
      .customerBox {
        height: 75%;
        display: flex;
        align-items: center;
        .tickContainer {
          width: 100%;
          display: flex;
          align-items: center;
          padding: 30px 10px;
          border-radius: 5px;
          justify-content: space-between;
          .tickName {
            width: 155px;
            color:  #606266;
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
          }
          .tickPrice {
            width: 80px;
            text-align: right;
            // overflow: hidden;
            // text-overflow: ellipsis;
            // white-space: nowrap;
          }
        }
      }
    }
  }
  .default {
    .tickContainer {
      .tickName {
        display: flex;
        justify-content: center;
        width: 100%!important;
      }
    }
  }
  ::v-deep {
    .el-calendar-table:not(.is-range) td.next,.el-calendar-table:not(.is-range) td.prev{
      pointer-events: none;
      background-color: #B0C9DD;
      // border-bottom: 5px solid #dfe6ec;
      // border-right: 5px solid #dfe6ec;
    }
  }
  ::v-deep thead th {
    font-size: 13px;
    font-weight: 800;
  }
  
  ::v-deep td.disabledDays {
    pointer-events: none;
    background-color: rgba(0, 0, 0, .3);
    color: #fff;
  }
  .money-init {
    width: 100%;
    text-align: right;
    margin-bottom: 10px;
    color: #606266;
    font-weight: 600;
  }
</style>

END...