以下是第一版的实现思路,第二版做了优化,包括实现思路、样式转由 template 控制等,代码更简洁,已封装成工具——v-calendar-pick

效果:

elementui年月选择器限制范围 vue 年份选择器_前端

部分代码

.vue

<template>
  <div class="date-select">
    <div class="date-select__custom">
      <span class="date-select__custom--text">自定义日期</span>
      <span class="date-select__custom--value">
        <span v-if="dateData.length" v-html="dateText"></span>
        <span v-else>请选择日期</span>
      </span>
    </div>
    <div class="date-select__header" v-if="loading">
      <div class="date-select__header-prev">
        <span class="date-select__btn date-select__btn-prev--year"><<</span>
        <span class="date-select__btn date-select__btn-prev--month"><</span>
      </div>

      <div class="date-select__header-middle">{{ year }} 年 {{ month }} 月</div>

      <div class="date-select__header-next">
        <span class="date-select__btn date-select__btn-next--month">></span>
        <span class="date-select__btn date-select__btn-next--year">>></span>
      </div>
    </div>
    <div class="date-select__body">
      <div class="date-select__weeks">
        <div class="date-select__week" v-for="i in week" :key="i">{{ i }}</div>
      </div>
      <div class="date-select__month" v-if="loading">
        <div :class="['date-select__day', i.date < i.showDate && 'prev-month', i.date > i.showDate && 'next-month']" v-for="i in monthData" :key="i.date">
          {{ i.showDate }}
        </div>
      </div>
    </div>
  </div>
</template>

js:

export default {
  name: 'date-select',
  props: {
    selectData: {
      type: Array,
      default: () => []
    },
    type: {
      type: String,
      default: 'date'
    },
    isOpen: {
      type: Boolean,
      default: false
    }
  },
  data() {
    return {
      monthData: [],
      week: ['日', '一', '二', '三', '四', '五', '六'],
      year: '',
      month: '',
      start: null,
      end: null,
      dateData: [],
      oldDateData: [],
      loading: true
    }
  },
  watch: {
    isOpen: {
      handler(val) {
        if (val) {
          this.dateData = this.selectData
          const [startDateArr, endDateArr] = [this.dateData?.[0]?.split('-'), this.dateData?.[1]?.split('-')]
          this.start = startDateArr == null ? null : new Date(startDateArr[0], startDateArr[1] - 1, startDateArr[2])
          this.end = endDateArr == null ? null : new Date(endDateArr[0], endDateArr[1] - 1, endDateArr[2])
          // 更多筛选是 if 销毁
          if (this.atomic) {
            this.oldDateData = this.selectData
          }
          // 默认筛选框是 show 销毁
          this.init()
        }
      },
      immediate: true
    }
  },
  computed: {
    dateText() {
      return `<span class="start-date">${this.dateData?.[0]}${
        this.dateData?.[1] == null ? '</span>' : '</span> 至 <span class="end-date">' + this.dateData?.[1]
      }`
    }
  },
  mounted() {
    this.addEvent()
  },
  methods: {
    addEvent() {
      const $datePickerDom = document.querySelector('.date-select')
      $datePickerDom.addEventListener(
        'click',
        e => {
          e.stopPropagation()
          const $target = e.target
          const targetClassList = $target.classList
          if (
            (!targetClassList.contains('date-select__btn') &&
              !targetClassList.contains('date-select__day') &&
              !targetClassList.contains('start-date') &&
              !targetClassList.contains('end-date')) ||
            targetClassList.contains('date-select__btn--disabled') ||
            targetClassList.contains('date-select__day--disabled')
          ) {
            return
          }

          if (targetClassList.contains('date-select__btn') || targetClassList.contains('start-date') || targetClassList.contains('end-date')) {
            let fixDate = null
            if (targetClassList.contains('date-select__btn-prev--year')) {
              fixDate = this.fixMonthToYear(this.year - 1, this.month)
            } else if (targetClassList.contains('date-select__btn-prev--month')) {
              fixDate = this.fixMonthToYear(this.year, this.month - 1)
            } else if (targetClassList.contains('date-select__btn-next--month')) {
              fixDate = this.fixMonthToYear(this.year, this.month + 1)
            } else if (targetClassList.contains('date-select__btn-next--year')) {
              fixDate = this.fixMonthToYear(this.year + 1, this.month)
            } else {
              // 点击日期跳转到对应日期
              const dateArr = $target.innerHTML.split('-')
              fixDate = this.fixMonthToYear(+dateArr[0], +dateArr[1])
            }
            this.setMonthDate(fixDate.year, fixDate.month)
          } else if (targetClassList.contains('date-select__day')) {
            const dayType = this.judgeDayType(targetClassList)
            // 设置日期开始和结束时间,并返回设置的状态
            this.setStartEndDate(dayType, $target)
            this.dateData = [this.formatDate(this.start), this.formatDate(this.end)]
          }

          this.reRender()
        },
        false
      )
    },
    swipeRight() {
      const fixDate = this.fixMonthToYear(this.year, this.month - 1)
      this.setMonthDate(fixDate.year, fixDate.month)
      this.reRender()
    },
    swipeLeft() {
      const fixDate = this.fixMonthToYear(this.year, this.month + 1)
      this.setMonthDate(fixDate.year, fixDate.month)
      this.reRender()
    },
    // 重新渲染
    reRender() {
      // 通过开关 loading,来清空已点击选项
      this.clearUI()
      this.$nextTick().then(() => {
        this.renderUI()
        this.renderClickUI()
      })
    },
    init(year = this.start?.getFullYear(), month = this.start?.getMonth() + 1) {
      this.setMonthDate(year, month)
      this.reRender()
    },
    // 渲染未来时间不可选
    renderUI() {
      const fixDate = this.fixMonthToYear(this.year, this.month + 1)
      const date = new Date(fixDate.year, fixDate.month - 1)
      const today = new Date()
      // 下一年、下一月
      if (date - today > 0) {
        const monthDomClassList = document.getElementsByClassName('date-select__btn-next--month')[0].classList
        const yearDomClassList = document.getElementsByClassName('date-select__btn-next--year')[0].classList
        monthDomClassList.add('date-select__btn--disabled')
        yearDomClassList.add('date-select__btn--disabled')
      }
      // 下一天
      const dateDoms = document.getElementsByClassName('date-select__day')
      const dateDomsLength = dateDoms.length
      for (let i = 0; i < dateDomsLength; i++) {
        const dateData = this.monthData[i]
        const date = new Date(dateData?.year, dateData?.month - 1, dateData?.showDate)
        if (date - today > 0) {
          while (i < dateDomsLength) {
            const domClassList = dateDoms[i].classList
            domClassList.add('date-select__day--disabled')
            i++
          }
        }
      }
    },
    // 通过开始和结束时间,设置页面点击样式
    renderClickUI() {
      // 没点击,则无需渲染
      if (!this.start) {
        return
      }

      // 获取当前渲染日期界面的第一个和最后一个日期
      const [first, last] = [this.monthData[0], this.monthData[this.monthData.length - 1]]
      // 当点击的时间不在开始和结束的范围内,则无需渲染
      const firstDate = new Date(first.year, first.month - 1, first.showDate)
      const lastDate = new Date(last.year, last.month - 1, last.showDate)
      if (this.start - lastDate > 0 || (this.end && this.end - firstDate < 0)) {
        return
      }

      // 赋值新的点击、包括中间状态、开始和结束
      // 遍历每个日期
      const dateDoms = document.getElementsByClassName('date-select__day')
      for (let i = 0; i < dateDoms.length; i++) {
        const dateData = this.monthData[i]
        const date = new Date(dateData.year, dateData.month - 1, dateData.showDate)
        const domClassList = dateDoms[i].classList
        // 只有开始,给开始设置样式
        if (!this.end && this.start - date === 0) {
          domClassList.add('date-select__day--start')
          break
        }
        // 开始和结束都有
        // 设置开始
        if (this.start - date === 0) {
          // 当点击相同的
          if (this.end === this.start) {
            domClassList.remove('date-select__day--start')
            this.start = null
            this.end = null
            this.dateData = []
            break
          } else {
            // 给开始添加范围样式
            domClassList.add('date-select__day--start', 'select-both')
          }
          continue
        } else if (this.start - date < 0 && date - this.end < 0) {
          // 渲染中间的样式
          domClassList.add('date-select__day--middle')
          continue
        } else if (this.end - date === 0) {
          // 渲染结束的样式
          domClassList.add('date-select__day--end', 'select-both')
          break
        }
      }
    },
    // 判断当前点击的日期是上个月,这个月,还是当前月份的
    judgeDayType(classList) {
      let dayType = 'currMonth'
      if (classList.contains('prev-month')) {
        dayType = 'prevMonth'
      } else if (classList.contains('next-month')) {
        dayType = 'nextMonth'
      }
      return dayType
    },
    // 设置开始和结束日期
    setStartEndDate(dayType, dom) {
      const day = dom.innerHTML

      let clickDate = null
      let fixDate = null
      switch (dayType) {
        case 'currMonth':
          fixDate = this.fixMonthToYear(this.year, this.month)
          break
        case 'prevMonth':
          fixDate = this.fixMonthToYear(this.year, this.month - 1)
          break
        case 'nextMonth':
          fixDate = this.fixMonthToYear(this.year, this.month + 1)
          break
      }
      clickDate = new Date(fixDate.year, fixDate.month - 1, day)

      // 清空已选的开始和结束,只点击了开始
      if (!this.start || (this.start && this.end)) {
        this.start = clickDate
        this.end = null
      } else if (this.start - clickDate === 0) {
        // 开始和结束都在同一天
        this.end = this.start
      } else if (this.start - clickDate > 0) {
        // 交换结束和开始
        this.end = this.start
        this.start = clickDate
      } else {
        // 设置结束
        this.end = clickDate
      }
    },
    setMonthDate(year, month) {
      // 若没有传入年月,则默认选择当前年月
      if (!year || !month) {
        const today = new Date()
        year = today.getFullYear()
        month = today.getMonth() + 1 // 例如想要的是12月,getMonth() 会返回 11(js 里的 month 总是会比实际的少 1)
      }

      // 当月第一天相关
      let firstDateOfCurrMonth = new Date(year, month - 1, 1) // 获取当月第一天
      let weekOfFirstDate = firstDateOfCurrMonth.getDay() // 当月第一天是一周的星期几。那么之前的时间是上个月的

      // 周日
      if (weekOfFirstDate === 0) {
        weekOfFirstDate = 7
      }

      // 上个月最后一天相关
      let lastDateOfLastMonth = new Date(year, month - 1, 0) // 上个月最后一天 年月日
      let lastDayOfLastMonth = lastDateOfLastMonth.getDate() // 上个月最后一天 日

      let preMonthDayCount = weekOfFirstDate // 本月第一行,留有上个月的天数。因为是从周日开始的。若 1 号为周二,那么留有上个月的天数为 2(周日、周一)

      let lastDateOfCurrMonth = new Date(year, month, 0) // 本月的最后一天 年月日
      let lastDayOfCurrMonth = lastDateOfCurrMonth.getDate() // 本月的最后一天 日

      // 设置月的范围
      let range = 5 * 7
      let allDayCount = lastDayOfCurrMonth + preMonthDayCount
      if (allDayCount > range) {
        // 会有 6 行
        range += 7
      } else if (allDayCount <= range - 7) {
        // 只有 4 行
        range -= 7
      }

      this.monthData = this.getMonthData(range, year, month, preMonthDayCount, lastDayOfLastMonth, lastDayOfCurrMonth)

      this.year = year
      this.month = month
    },
    getMonthData(range, year, month, preMonthDayCount, lastDayOfLastMonth, lastDayOfCurrMonth) {
      const ret = []
      let date, showDate, currMonth

      for (let i = 0; i < range; i++) {
        date = i + 1 - preMonthDayCount // 本月的几号
        showDate = date
        currMonth = month

        // 上个月
        if (date <= 0) {
          currMonth = month - 1
          showDate = lastDayOfLastMonth + date // 上个月的第几号
        } else if (date > lastDayOfCurrMonth) {
          // 下一个月
          currMonth = month + 1
          showDate = showDate - lastDayOfCurrMonth // 下个月的第几号
        }

        const fixDate = this.fixMonthToYear(year, currMonth)

        ret.push({
          year: fixDate.year,
          month: fixDate.month,
          date,
          showDate
        })
      }
      return ret
    },
    fixMonthToYear(year, month) {
      // 若本月为 1 月,则上一月为 12 月
      if (month === 0) {
        month = 12
        year -= 1
      }
      // 若本月为 12 月,则下一月为 1 月
      if (month === 13) {
        month = 1
        year += 1
      }

      year = year || 0

      return { year, month }
    },
    formatDate(date) {
      if (!date) {
        return date
      }
      return `${date.getFullYear()}-${date.getMonth() + 1 < 10 ? `0${date.getMonth() + 1}` : date.getMonth() + 1}-${
        date.getDate() < 10 ? `0${date.getDate()}` : date.getDate()
      }`
    },
    // 重新渲染日历
    clearUI() {
      this.loading = false
      this.$nextTick(() => {
        this.loading = true
      })
    },
    cancel() {
      if (this.atomic) {
        this.confirmData('', this.oldDateData)
      } else {
        // 日期重置回当月
        this.dateData = []
        this.start = null
        this.end = null
        this.setMonthDate()
        this.reRender()
      }
    },
    confirm(type) {
      this.confirmData(type, this.dateData)
    },
    confirmData(type, dateData) {
      const value = dateData[1] ? [dateData[0], dateData[1]] : dateData[0] ? [dateData[0]] : []
      this.$emit('confirm', type, value)
    }
  }
}