背景

很久没写博客了,最近接到产品的新需求,需要按季度汇总数据,可是这么重要的组件,Element 官方居然没有提供。网上逛了一圈,发现功能都比较单一,无法满足需求。于是决定整一个并记录下来,供大家参考,欢迎留言共同学习交流。

实现方式

秉承自己动手丰衣足食的原则,通过查看并借鉴 Element 官方 DatePicker 日期选择器 源码,使用 Popover 弹出框 封装了一个 季度选择器 组件 QuarterPicker,以期用法和功能尽量接近官方类似组件的用法,先来看下最终效果图,如下:

季度选择器.png

季度选择器 QuarterPicker 组件源码

在项目 components 路径下创建 quarter-picker 文件夹,并在其下创建 index.vue 文件,内容如下:

<!--
 * @Descripttion: 季度选择器
 * @version: 1.0
 * @Author: https://www.lervor.com/
 * @Date: 2021-12-06
 * @LastEditTime: 2021-12-09
-->
<template>
  <el-popover
    trigger="focus"
    v-model="pickerVisible"
    popper-class="lervor-quarter-popover"
    :disabled="disabled">
    <el-input
      ref="reference"
      slot="reference"
      class="el-date-editor"
      readonly
      :disabled="disabled"
      :size="size"
      :placeholder="placeholder"
      :value="displayValue"
      :validate-event="false"
      :style="{ width }"
      @mouseenter.native="handleMouseEnter"
      @mouseleave.native="showClose = false">
      <i slot="prefix"
        class="el-input__icon"
        :class="triggerClass">
      </i>
      <i slot="suffix"
        class="el-input__icon"
        :class="[showClose ? '' + clearIcon : '']"
        @click="handleClickIcon"
        @mousedown="handleMousedownIcon">
      </i>
    </el-input>
    <div class="lervor-quarter-picker">
      <div class="el-date-picker__header el-date-picker__header--bordered">
        <button
          type="button"
          aria-label="前一年"
          class="el-picker-panel__icon-btn el-date-picker__prev-btn el-icon-d-arrow-left"
          @click="prevYear">
        </button>
        <span
          role="button"
          class="el-date-picker__header-label">{{ yearLabel }}</span>
        <button
          type="button"
          aria-label="后一年"
          class="el-picker-panel__icon-btn el-date-picker__next-btn el-icon-d-arrow-right"
          @click="nextYear">
        </button>
      </div>
      <div class="el-picker-panel__content" style="width: 200px; margin: 10px 15px;">
        <table class="lervor-quarter-table" @click="handleTableClick">
          <tbody>
            <tr>
              <td class="available" :class="getCellStyle(0)">
                <a class="cell">第一季度</a>
              </td>
              <td class="available" :class="getCellStyle(1)">
                <a class="cell">第二季度</a>
              </td>
            </tr>
            <tr>
              <td class="available" :class="getCellStyle(2)">
                <a class="cell">第三季度</a>
              </td>
              <td class="available" :class="getCellStyle(3)">
                <a class="cell">第四季度</a>
              </td>
            </tr>
          </tbody>
        </table>
      </div>
    </div>
  </el-popover>
</template>

<script>
  import { formatDate, prevYear, nextYear, range, nextDate, isDateObject, parseDate } from 'element-ui/src/utils/date-util'
  import { hasClass } from 'element-ui/src/utils/dom'

  // 获取指定年份和季度的所有日期
  const datesInYearAndQuarter = (year, quarter) => {
    const numOfDays = getDayCountOfQuarter(year, quarter)
    const firstDay = new Date(year, quarter * 3, 1)
    return range(numOfDays).map(n => nextDate(firstDay, n))
  }

  // 获取指定年份和季度总天数
  const getDayCountOfQuarter = (year, quarter) => {
    switch(quarter) {
      case 0: // 第一季度包含二月,需要对是否闰年进行判断处理
        if (year % 4 === 0 && year % 100 !== 0 || year % 400 === 0) {
          return 91
        } else {
          return 90
        }
      case 1:
        return 91
      default:
        return 92
    }
  }

  export default {
    name: 'QuarterPicker',
    props: {
      size: String,
      format: String, // 显示在输入框中的格式,引入季度:q(阿拉伯数字)、Q(中文数字)
      valueFormat: String,
      placeholder: String,
      prefixIcon: String,
      clearIcon: {
        type: String,
        default: 'el-icon-circle-close'
      },
      disabled: Boolean,
      clearable: {
        type: Boolean,
        default: true
      },
      width: { // 组件宽度
        type: String,
        default: ''
      },
      disabledDate: {}, // 不可用的日期
      value: null
    },
    data() {
      return {
        showClose: false,
        pickerVisible: false,
        date: new Date(),
        quarterText: [ '一', '二', '三', '四' ]
      }
    },
    computed: {
      triggerClass() {
        return this.prefixIcon || 'el-icon-date'
      },
      displayValue() {
        if (!this.value) return null
        // 季度,从0开始
        const quarter = parseInt(this.parsedValue.getMonth() / 3)
        let fDate = formatDate(this.parsedValue, this.format)
        fDate = fDate.replace(/q/, quarter + 1).replace(/Q/, this.quarterText[quarter])
        return fDate
      },
      year() {
        return this.date.getFullYear()
      },
      yearLabel() {
        return this.year + ' 年'
      },
      parsedValue() {
        if (!this.value) {
          return this.value
        }
        if (isDateObject(this.value)) {
          return this.value
        }
        // 非时间格式且设置了valueFormat,进行时间转换
        if (this.valueFormat) {
          return parseDate(this.value, this.valueFormat)
        }
        // 非时间格式且未设置valueFormat,再尝试转换时间
        return new Date(this.value)
      }
    },
    watch: {
      value(value) {
        this.date = value ? this.parsedValue : new Date()
      }
    },
    methods: {
      handleMouseEnter() {
        if (this.disabled) return
        if (this.value && this.clearable) {
          this.showClose = true
        }
      },
      handleClickIcon(event) {
        if (this.disabled) return
        if (this.showClose) {
          this.$emit('input', null)
          this.$emit('change', null)
          this.showClose = false
          this.pickerVisible = false
          this.$refs.reference.blur()
        }
      },
      handleMousedownIcon(event) {
        // 阻止鼠标按下清空按钮,防止清空数据时季度选择面板闪现
        event.preventDefault()
      },
      handleTableClick(event) {
        let target = event.target
        if (target.tagName === 'A') {
          target = target.parentNode
        }
        if (target.tagName !== 'TD') return
        if (hasClass(target, 'disabled')) return
        const column = target.cellIndex
        const row = target.parentNode.rowIndex
        // 季度,从0开始
        const quarter = row * 2 + column
        // 季度开始月份,从0开始
        const month = quarter * 3
        let newDate = new Date(this.year, month, 1)
        if (this.valueFormat) {
          newDate = formatDate(newDate, this.valueFormat)
        }
        this.pickerVisible = false
        this.$emit('input', newDate)
        this.$emit('change', newDate)
      },
      prevYear() {
        this.date = prevYear(this.date)
      },
      nextYear() {
        this.date = nextYear(this.date)
      },
      getCellStyle(quarter) {
        const style = {}
        const today = new Date()
        const date = this.parsedValue ? this.parsedValue : today
        style.disabled = typeof this.disabledDate === 'function'
          ? datesInYearAndQuarter(this.year, quarter).every(this.disabledDate) : false
        // 当前选中的季度样式
        style.current = date.getFullYear() === this.year && parseInt(date.getMonth() / 3) === quarter
        // 今日所在季度样式
        style.quarter = today.getFullYear() === this.year && parseInt(today.getMonth() / 3) === quarter
        return style
      }
    }
  }
</script>

<style>
  .lervor-quarter-picker {
    line-height: 30px;
  }
  .lervor-quarter-popover {
    padding: 0;
  }
  .lervor-quarter-table {
    font-size: 12px;
    margin: -1px;
    border-collapse: collapse;
    width: 100%;
  }
  .lervor-quarter-table td {
    text-align: center;
    padding: 10px 3px;
    cursor: pointer;
  }
  .lervor-quarter-table td .cell {
    height: 32px;
    display: block;
    line-height: 32px;
    color: #606266;
    margin: 0 auto;
  }
  .lervor-quarter-table td .cell:hover {
    color: #1890ff;
  }

  .lervor-quarter-table td.current:not(.disabled) .cell {
    color: #409eff;
  }

  .lervor-quarter-table td.quarter .cell {
    color: #409eff;
    font-weight: 700;
  }

  .lervor-quarter-table td.disabled .cell {
    background-color: #F5F7FA;
    cursor: not-allowed;
    color: #C0C4CC;
  }
</style>

季度选择器 QuarterPicker 组件文档

Attributes

参数说明类型可选值默认值
value / v-model绑定值date
disabled禁用booleanfalse
clearable是否显示清除按钮booleantrue
size输入框尺寸stringlarge, small, mini
width组件宽度string
placeholder占位内容string
format显示在输入框中的格式,引入季度:q(阿拉伯数字)、Q(中文数字)string
value-format可选,绑定值的格式。不指定则绑定值为 Date 对象string
prefix-icon自定义头部图标的类名stringel-icon-date
clear-icon自定义清空图标的类名stringel-icon-circle-close
disabled-date不可用的日期function

Events

事件名称说明回调参数
change用户选择值时触发组件绑定值。格式与绑定值一致,可受 value-format 控制

注意:本组件获取到的值为所选季度的第一天日期,如 2021年第二季度,其值为 2021-04-01 00:00:00,类似官方 年/月选择器

季度选择器 QuarterPicker 组件的使用

简单使用如下:

<template>
  <div>
    <quarter-picker
      width="150px"
      format="yyyy年q季度"
      value-format="yyyyMM"
      placeholder="选择季度"
      v-model="quarterDate"
      :disabled-date="disabledQuarter"
      @change="handleChangeQuarter" />
  </div>
</template>

<script>
import QuarterPicker from '@/components/quarter-picker'
export default {
  components: {
    QuarterPicker
  },
  data() {
    return {
      quarterDate: '202104',
      // 禁用日期/季度示例,只允许选择本年度本季度(不含)之前的季度
      disabledQuarter: time => {
        const now = new Date()
        return time.getFullYear() > now.getFullYear() || time.getFullYear() === now.getFullYear() && parseInt(time.getMonth() / 3) >= parseInt(now.getMonth() / 3)
      }
    }
  },
  methods: {
    handleChangeQuarter(date) {
      console.info(date)
    }
  }
}
</script>

本例子的最终效果图如下:

季度选择器2.png

友情提示:也可将组件进行全局注册,方便后续直接使用。

如果觉得我的文章对你有用,请点个赞