实现element-plus年份范围选择器

1,743 阅读2分钟

上一篇文章介绍了如何实现elementPlus季度选择器,本文将介绍下如何实现年份范围选择器。

演示效果

时间切换.gif

实现思路

实现方式基本与季度的差别不大,区别就是单元格的计算,季度的选择器只是显示当年年份的季度各自,年份需要根据范围来渲染。首先完成年份的切换,这里我按照element-plus的年份选择器一次切换10个年份,可以根据需求自行调整, 接下来就是按照切换的年份范围,渲染对应的单元格,给单元格设置样式。

与上一篇季度选择器代码有点不同,组件接受的参数名称发生了改变,defaultValue改为了modelValue,使用是可以通过v-model进行绑定,不需要在监听组件事件修改传入值。

完整代码

  <div class="el-picker-panel el-date-picker year-panel">
    <div class="el-picker-panel__body-wrapper">
      <div class="el-picker-panel__body">
        <div class="el-date-picker__header el-date-picker__header--bordered">
          <span class="el-date-picker__prev-btn">
            <button
              type="button"
              class="d-arrow-left el-picker-panel__icon-btn"
              @click="moveByYear(false)"
            >
              <el-icon><d-arrow-left /></el-icon>
            </button>
          </span>
          <span
            role="button"
            class="el-date-picker__header-label"
            aria-live="polite"
            tabindex="0"
            >{{ yearLabel }}</span
          >
          <span class="el-date-picker__next-btn">
            <button
              type="button"
              class="el-picker-panel__icon-btn d-arrow-right"
              @click="moveByYear(true)"
            >
              <el-icon><d-arrow-right /></el-icon>
            </button>
          </span>
        </div>
        <div class="el-picker-panel__content">
          <BasicYearTable
            :min-date="minDate"
            :max-date="maxDate"
            :date="innerDate"
            :range-state="rangeState"
            :disabled-date="disabledDate"
            @pick="handleRangePick"
            @select="onSelect"
            @changerange="handleChangeRange"
          />
        </div>
      </div>
    </div>
  </div>
</template>
<script>
import { defineComponent } from 'vue'
import { useDatePicker } from '../../hooks/useDatePicker'
import { useDatePanel } from './hooks/useDatePanel'
import BasicYearTable from './src/BasicYearTable'
export default defineComponent({
  name: 'YearPanel',
  components: {
    BasicYearTable
  },
  props: {
    range: {
      type: Array,
      default() {
        return null
      }
    },
    modelValue: {
      type: Array,
      require: true,
      default() {
        return null
      }
    }
  },
  emits: ['change', 'update:modelValue'],
  setup(props) {
    const {
      minDate,
      maxDate,
      disabledDate,
      rangeState,
      onSelect,
      handleRangePick,
      handleChangeRange
    } = useDatePicker(props)

    const { innerDate, moveByYear, yearLabel } = useDatePanel({
      minDate
    })

    return {
      minDate,
      maxDate,
      innerDate,
      rangeState,
      yearLabel,
      moveByYear,
      disabledDate,
      handleRangePick,
      onSelect,
      handleChangeRange
    }
  }
})
</script>
<style>
.year-panel {
  width: 100%;
}
.year-panel .el-picker-panel__content {
  width: 100%;
  margin: 0px;
}
</style>
useDatePanel.js
import { computed, ref } from 'vue'
import _ from 'lodash'
export const useDatePanel = ({ minDate }) => {
  const innerDate = ref(_.cloneDeep(minDate.value))

  const year = computed(() => {
    return innerDate.value.year()
  })

  const yearLabel = computed(() => {
    const startYear = Math.floor(year.value / 10) * 10
    return `${startYear} - ${startYear + 9}`
  })

  /**
   * 切换时间
   * @param {*} forward
   */
  const moveByYear = (forward) => {
    const currentDate = innerDate.value
    const action = forward ? 'add' : 'subtract'
    innerDate.value = currentDate[action](10, 'year')
  }
  return {
    innerDate,
    yearLabel,
    moveByYear
  }
}
import _ from 'lodash'
import { getCurrentInstance, watch, ref, unref } from 'vue'
import dayjs from 'dayjs'
// 将计算时间的相关操作单独抽一个hooks 之后做年份的时候可以复用 季度,年份时间单位不同而已
export const useDatePicker = (props) => {
  // 时间范围 超出该范围禁用
  const { range } = props

  const { emit } = getCurrentInstance()
  // 默认值起始时间
  const minDate = ref()
  // 默认值结束时间
  const maxDate = ref()
  // 左侧面板时间 ps:面板时间与默认值时间无关 只是用于面板title显示 不需要过于纠结
  const leftDate = ref()
  // 右侧面板时间
  const rightDate = ref()

  const rangeState = ref({
    endDate: null,
    selecing: false
  })

  const disabledDate = (cell) => {
    // range 不存在  不做禁用
    if (!range) {
      return false
    }

    const [start, end] = range.map((item) => dayjs(item))

    return !(
      dayjs(cell).isSameOrAfter(start) && dayjs(cell).isSameOrBefore(end)
    )
  }

  const onSelect = (selecting) => {
    rangeState.value.selecting = selecting
    if (!selecting) {
      rangeState.value.endDate = null
    }
  }
  
  const handleChangeRange = (val) => {
    rangeState.value = val
  }
  
  const isValidRange = (range) => {
    if (!_.isArray(range)) return false

    const [left, right] = range

    return (
      dayjs.isDayjs(left) && dayjs.isDayjs(right) && left.isSameOrBefore(right)
    )
  }

  const handleRangeConfirm = () => {
    const _minDate = unref(minDate)
    const _maxDate = unref(maxDate)
    if (isValidRange([_minDate, _maxDate])) {
      emit(
        'change',
        [_minDate, _maxDate].map((_) => _.toDate())
      )
      emit(
        'update:modelValue',
        [_minDate, _maxDate].map((_) => _.toDate())
      )
    }
  }

  const handleRangePick = (val) => {
    const minDate_ = val.minDate
    const maxDate_ = val.maxDate
    if (maxDate.value === maxDate_ && minDate.value === minDate_) {
      return
    }
    maxDate.value = maxDate_
    minDate.value = minDate_
    handleRangeConfirm()
  }

  const restoreDefault = () => {
    // 将默认转为dayjs时间对象
    const [start, end] = props.modelValue.map((item) => dayjs(item))
    minDate.value = start
    maxDate.value = end
    leftDate.value = start
    rightDate.value = start.add(1, 'year')
  }

  watch(
    () => props.modelValue,
    (value) => {
      if (value) {
        restoreDefault()
      }
    },
    {
      immediate: true
    }
  )
  return {
    minDate,
    maxDate,
    leftDate,
    rightDate,
    rangeState,
    disabledDate,
    handleRangePick,
    onSelect,
    handleChangeRange
  }
}
<template>
  <table
    role="grid"
    class="el-year-table el-month-table year-table"
    @click="handleYearTableClick"
    @mousemove="handleMouseMove"
  >
    <tbody>
      <tr v-for="(row, key) in rows" :key="key">
        <td
          v-for="(cell, key_) in row"
          :key="key_"
          :class="getCellStyle(cell)"
          @click="handleMonthTableClick"
        >
          <div>
            <span class="cell">
              {{ cell.text }}
            </span>
          </div>
        </td>
      </tr>
    </tbody>
  </table>
</template>
<script>
import { computed, defineComponent, ref } from 'vue'
import dayjs from 'dayjs'
import { hasClass } from '@/date-picker-panel/utils'
export default defineComponent({
  name: 'BasicYearTable',
  props: {
    minDate: {
      type: Object,
      default() {
        return null
      }
    },
    maxDate: {
      type: Object,
      default() {
        return null
      }
    },
    date: {
      type: Object,
      default() {
        return null
      }
    },

    disabledDate: {
      type: Function,
      default() {
        return null
      }
    },
    rangeState: {
      type: Object,
      default() {
        return null
      }
    }
  },
  setup(props, { emit }) {
    const lastRow = ref()
    const lastColumn = ref()
    const tableRows = ref([[], [], []])

    const startYear = computed(() => {
      return Math.floor(props.date.year() / 10) * 10
    })
    
    // 设置单元格样式
    const getCellStyle = (cell) => {
      const style = {}
      const cellYear = cell.text

      style.disabled = props.disabledDate
        ? props.disabledDate(dayjs().startOf('year').year(cellYear))
        : false

      style.today = cell.type === 'today'

      if (cell.inRange) {
        style['in-range'] = true

        if (cell.start) {
          style['start-date'] = true
        }

        if (cell.end) {
          style['end-date'] = true
        }
      }
      return style
    }

    const rows = computed(() => {
      const rows = tableRows.value
      const now = dayjs().startOf('year')

      for (let i = 0; i < 3; i++) {
        const row = rows[i]
        for (let j = 0; j < 4; j++) {
          const index = i * 4 + j
          // 只显示10个单元格 超出跳出循环
          if (index > 9) {
            continue
          }
          const cell = (row[j] ||= {
            row: i,
            column: j,
            type: 'normal',
            inRange: false,
            start: false,
            end: false,
            disabled: false
          })

          cell.type = 'normal'

          const calTime = props.date.startOf('year').add(index, 'year')

          const calEndDate =
            props.rangeState.endDate ||
            props.maxDate ||
            (props.rangeState.selecting && props.minDate) ||
            null

          cell.inRange =
            !!(
              props.minDate &&
              calTime.isSameOrAfter(props.minDate, 'year') &&
              calEndDate &&
              calTime.isSameOrBefore(calEndDate, 'year')
            ) ||
            !!(
              props.minDate &&
              calTime.isSameOrBefore(props.minDate, 'year') &&
              calEndDate &&
              calTime.isSameOrAfter(calEndDate, 'year')
            )

          if (props.minDate?.isSameOrAfter(calEndDate)) {
            cell.start = !!(calEndDate && calTime.isSame(calEndDate, 'year'))
            cell.end = props.minDate && calTime.isSame(props.minDate, 'year')
          } else {
            cell.start = !!(
              props.minDate && calTime.isSame(props.minDate, 'year')
            )
            cell.end = !!(calEndDate && calTime.isSame(calEndDate, 'year'))
          }

          const isToday = now.isSame(calTime)

          if (isToday) {
            cell.type = 'today'
          }

          cell.text = startYear.value + i * 4 + j
          cell.disabled = props.disabledDate?.(calTime.toDate()) || false
        }
      }
      return rows
    })

    const handleMouseMove = (event) => {
      if (!props.rangeState.selecting) return

      let target = event.target

      if (target.tagName === 'A') {
        target = target.parentNode?.parentNode
      }
      if (target.tagName === 'DIV') {
        target = target.parentNode
      }
      if (target.tagName !== 'TD') return

      const row = target.parentNode.rowIndex
      const column = target.cellIndex

      if (rows.value[row][column].disabled) return

      if (row !== lastRow.value || column !== lastColumn.value) {
        lastRow.value = row
        lastColumn.value = column

        emit('changerange', {
          selecting: true,
          endDate: props.date.startOf('year').add(row * 4 + column, 'year')
        })
      }
    }

    const handleMonthTableClick = (event) => {
      const target = event.target.closest('td')

      if (target?.tagName !== 'TD') return
      if (hasClass(target, 'disabled')) return
      const column = target.cellIndex

      const row = target.parentNode.rowIndex

      const year = row * 4 + column
      const newDate = props.date.startOf('year').add(year, 'year')

      if (!props.rangeState.selecting) {
        emit('pick', { minDate: newDate, maxDate: null })
        emit('select', true)
      } else {
        if (props.minDate && newDate >= props.minDate) {
          emit('pick', { minDate: props.minDate, maxDate: newDate })
        } else {
          emit('pick', { minDate: newDate, maxDate: props.minDate })
        }
        emit('select', false)
      }
    }

    return {
      rows,
      getCellStyle,
      handleMouseMove,
      handleMonthTableClick
    }
  }
})
</script>
<style>
.year-table td {
  padding: 20px 0px;
}
</style>