由于在数据项目中经常以各种时间条件查询数据,所以时间选择器(DatePicker)组件是很常用的组件。但是在我使用的 Element UI 中,缺少了季度选择器的功能。

简易实现

一开始我根据时间范围使用 select 去遍历,如 2024-Q1、2023-Q4、2023-Q3 如此类推。

image

element 并无季度选择器

其实也算是快速解决了 element ui 无法选择季度的问题。但总感觉特别的 low,后来有时间了就去隔壁 ant design 看了看。

image

发现在新版的 ant design 都支持季度和季度范围选择器了……

查了查新的 element plus 也只是只支持了 'year' | 'years' |'month' | 'date' | 'dates' | 'datetime' | 'week' | 'datetimerange' | 'daterange' | 'monthrange' 这些个类型。

工具不给力,又不想用其他库的情况下只能手搓了。

手搓季度选择器

季度面板

参考 ant design 做了一个类似的面板。

image

<template>
  <div class="quarter-panel">
    <div class="quarter-panel-header">
      <i
        class="quarter-panel-header-icon el-icon-arrow-left"
        @click="currentYear--"
      />
      <div class="quarter-panel-header-title">{{ currentYear }} 年</div>
      <i
        class="quarter-panel-header-icon el-icon-arrow-right"
        @click="currentYear++"
      />
    </div>
    <div class="quarter-panel-content">
      <div
        v-for="option in quarterOptions"
        class="quarter-panel-item-btn"
        :class="getComputedClass(option.value)"
        :key="option.value"
        @click="emitClick(option.value)"
      >
        {{ option.label }}
      </div>
    </div>
  </div>
</template>

<script>
import dayjs from 'dayjs'
import customParseFormat from 'dayjs/plugin/customParseFormat'

dayjs.extend(customParseFormat)

export default {
  name: 'QuarterPanel',
  props: {
    value: String,
    dice: Number,
    min: String,
    max: String,
    todayDisabled: Boolean,
    featureDisabled: Boolean,
  },
  data() {
    return {
      currentYear: 2023,
    }
  },
  computed: {
    day() {
      if (this.value) {
        return dayjs(this.value, 'YYYY-MM-DD')
      }
      return dayjs()
    },
    computedDate() {
      return this.day.startOf('quarter').format('YYYY-MM-DD')
    },
    quarterOptions() {
      return [
        { label: 'Q1', value: `${this.currentYear}-01-01` },
        { label: 'Q2', value: `${this.currentYear}-04-01` },
        { label: 'Q3', value: `${this.currentYear}-07-01` },
        { label: 'Q4', value: `${this.currentYear}-10-01` },
      ]
    },
  },
  mounted() {
    this.currentYear = dayjs().year()
  },
  methods: {
    getDisabled(value) {
      let isFeature = false

      if (this.todayDisabled) {
        isFeature = dayjs()
          .subtract(1, 'day')
          .startOf('quarter')
          .isBefore(dayjs(value))
      } else if (this.featureDisabled) {
        isFeature = dayjs().startOf('quarter').isBefore(dayjs(value))
      }

      const isMin = this.min
        ? dayjs(this.min, 'YYYY-MM-DD')
            .startOf('quarter')
            .isAfter(dayjs(value, 'YYYY-MM-DD'))
        : false

      const isMax = this.max
        ? dayjs(this.max, 'YYYY-MM-DD')
            .startOf('quarter')
            .isBefore(dayjs(value, 'YYYY-MM-DD'))
        : false

      return isFeature || isMin || isMax
    },
    getComputedClass(value) {
      if (this.computedDate === value) {
        return 'quarter-panel-item-btn-active'
      }
      if (this.getDisabled(value)) {
        return 'quarter-panel-item-btn-disabled'
      }
      return ''
    },
    emitClick(value) {
      if (this.getDisabled(value)) {
        return
      }
      this.$emit('input', value)
    },
  },
  watch: {
    dice() {
      this.currentYear = this.day.year()
    },
  },
}
</script>

<style lang="scss" scoped>
$--gw-primary-color: #f6674f;

.quarter-panel {
  width: 200px;
  color: #303133;

  .quarter-panel-header {
    height: 30px;
    padding: 12px;
    display: flex;
    align-items: center;

    .quarter-panel-header-icon {
      font-size: 12;
      margin: 5px;
      cursor: pointer;

      &:hover {
        color: $--gw-primary-color;
      }
    }

    .quarter-panel-header-title {
      flex: 1;
      text-align: center;
      font-size: 16;
    }
  }

  .quarter-panel-content {
    display: flex;
    align-items: center;

    .quarter-panel-item-btn {
      flex: 1;
      font-size: 14;
      height: 30px;
      line-height: 30px;
      text-align: center;
      cursor: pointer;
      border: solid 1px transparent;
      border-radius: 5px;

      &:hover {
        color: $--gw-primary-color;
        border: solid 1px $--gw-primary-color;
      }
    }

    .quarter-panel-item-btn-active {
      background: $--gw-primary-color;
      color: #ffffff;

      &:hover {
        color: #ffffff;
      }
    }

    .quarter-panel-item-btn-disabled {
      color: #909399;
      background: #f2f6fc;
      cursor: not-allowed;

      &:hover {
        color: #909399;
        background: #f2f6fc;
      }
    }
  }
}
</style>

季度选择器

将面板放到 el-popover 中实现类似 DatePicker 的效果。并且提供了像清空数据、最大值、最小值等常用功能。

image

<template>
  <div class="quarter-picker" :class="{ 'quarter-picker-disabled': disabled }">
    <div class="quarter-picker-date-button">
      <i class="iconfont icon-date-select-icon quarter-picker-time-icon" />
      <el-popover
        placement="bottom-start"
        width="200"
        trigger="click"
        ref="datePopover"
        :disabled="disabled"
        @show="initPopover"
      >
        <div
          class="quarter-picker-date-button-item quarter-picker-date-button-item-long"
          slot="reference"
        >
          <span v-if="form.date" class="button-item-span">
            {{ dateQuarterStr }}
          </span>
          <span v-else class="button-item-span">选择时间</span>
          <div class="bottom-line" />
        </div>
        <quarterPanel
          v-model="form.date"
          :dice="dice"
          :min="min"
          :max="max"
          :featureDisabled="featureDisabled"
          :todayDisabled="todayDisabled"
          @input="emitDateChange()"
        />
      </el-popover>
      <i
        v-show="form.date && clearable"
        class="el-icon-close quarter-picker-clear-icon"
        @click.stop="clearCurrentDate"
      />
    </div>
  </div>
</template>

<script>
import dayjs from 'dayjs'
import customParseFormat from 'dayjs/plugin/customParseFormat'
import quarterOfYear from 'dayjs/plugin/quarterOfYear'

import quarterPanel from './quarterPanel.vue'

dayjs.extend(customParseFormat)
dayjs.extend(quarterOfYear)

/**
 * date 日期
 */
export default {
  name: 'QuarterPicker',
  components: {
    quarterPanel,
  },
  props: {
    date: String,
    min: String,
    max: String,
    featureDisabled: Boolean,
    todayDisabled: Boolean,
    disabled: Boolean,
    clearable: Boolean,
  },
  data() {
    return {
      form: {
        date: '',
      },
      dice: 0,
    }
  },
  mounted() {
    this.syncData()
  },
  computed: {
    dateQuarterStr() {
      if (!this.form.date) return '选择季'

      const dj = dayjs(this.form.date).startOf('quarter')
      return `${dj.year()}-Q${dj.quarter()}`
    },
  },
  methods: {
    initPopover() {
      this.dice++
    },
    syncData() {
      this.form.date = this.date
    },
    clearCurrentDate() {
      if (this.disabled) return
      this.form.date = ''

      this.emitDateChange()
    },
    emitDateChange() {
      this.$emit('change', this.form)

      this.closePopovers()
    },
    closePopovers() {
      this.$refs.datePopover.doClose()
    },
  },
  watch: {
    date() {
      if (this.form.date !== this.date) {
        this.syncData()
      }
    },
  },
}
</script>

<style scoped lang="scss">
$--gw-primary-color: #f6674f;

.quarter-picker {
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: flex-end;

  .quarter-picker-date-button {
    display: flex;
    position: relative;
    flex-direction: row;
    align-items: center;
    justify-content: center;
    user-select: none;
    margin-left: 5px;
    width: 250px;
    padding-left: 10px;
    height: 28px;
    background: #ffffff;
    border: 1px solid #dcdfe6;
    font-size: 14px;
    font-family: Microsoft YaHei;
    font-weight: 400;
    color: #282c32;
    border-radius: 4px;

    .quarter-picker-time-icon {
      position: absolute;
      left: 12px;
    }

    .quarter-picker-date-button-item {
      position: relative;
      height: 28px;
      line-height: 28px;
      text-align: center;
      width: 70px;
      cursor: pointer;

      .bottom-line {
        position: absolute;
        bottom: 0;
        left: 0;
        right: 0;
        height: 2px;
        border-radius: 1px;
        background: transparent;
      }

      &:hover {
        .bottom-line {
          background: $--gw-primary-color;
        }
      }
    }

    .quarter-picker-date-button-item-long {
      width: 200px;

      .button-item-span {
        display: inline-block;
        width: 90px;
        text-align: center;
      }

      .button-item-span-active {
        color: $--gw-primary-color;
      }
    }

    .quarter-picker-clear-icon {
      position: absolute;
      right: 12px;
      font-size: 14;
      cursor: pointer;

      &:hover {
        color: $--gw-primary-color;
      }
    }
  }
}

.quarter-picker-disabled {
  .quarter-picker-date-button {
    color: #c0c4cc;
    background-color: #f2f6fc;

    .quarter-picker-date-button-item {
      cursor: not-allowed;
    }
  }

  .quarter-picker-date-button-item {
    &:hover {
      .bottom-line {
        background: transparent !important;
      }
    }
  }

  .quarter-picker-clear-icon {
    cursor: not-allowed !important;

    &:hover {
      color: #c0c4cc !important;
    }
  }
}
</style>

组件的使用

最后就是组件的使用了:

  <QuarterPicker
    type="quarter"
    :date="quarter.date"
    :min="minDate"
    :max="maxDate"
    :featureDisabled="options.featureDisabled"
    :todayDisabled="options.todayDisabled"
    :disabled="options.disabled"
    :clearable="options.clearable"
    @change="handleQuarterPickerChange"
  />
handleQuarterPickerChange({ date }) {
  this.quarter.date = date

  this.$message({
    message: '触发查询请求',
    type: 'success',
  })
},

最后

另外,季度范围选择器也可以用类似的思路来实现。以上就是个人解决季度选择器的方式。希望能对有类似需求的同学一些帮助。


VioletJack
931 声望161 粉丝

专注于Vue前端开发的学习和分享