vue 账期 - 季度或者半年类型的时间(区间)选择组件

引言

需求如文章标题,查阅资料发现 element-ui 并没有做支持这样类型的时间选择器的打算,所以只能手动造轮子。

组件经过一次改版,之前无法进行默认值的设置,后来改造之后,采用双向数据绑定驱动 组件 => 页面;页面 => 组件 的方式。

具备功能:默认时间、时间类型(可选)、是否区间(可选)。

正文

  1. 演示效果
账期 - 季度或者半年类型的时间选择组件 - 区间.gif
账期 - 季度或者半年类型的时间选择组件 - 非区间.gif
  1. 源码
  • 组件 QuarterOrHalfyear.vue
<template lang='pug'>
//- 账期 - 季度或者半年类型的时间(区间)选择组件
.quarter-or-halfyear
  //- 遮罩,为了点击空白处关闭时间选择框
  .mask-block(v-show='showChooseBox', @click.stop='handlerCancel')
  //- input 展示框
  el-input(
    v-model='showName',
    @focus='showChooseBox = true',
    placeholder='请选择',
    size='small'
  )
    i.el-input__icon.el-icon-date(slot='prefix')
  //- 时间选择框
  .date-choose-box(
    v-show='showChooseBox',
    :class='{ "date-choose-box-shot": !isInterval }'
  )
    //- 左侧
    .box-left
      .box-year
        button.el-picker-panel__icon-btn.el-icon-d-arrow-left(
          type='button',
          @click='leftYear--'
        )
        .left-year {{ leftYear }}
        button.el-picker-panel__icon-btn.el-icon-d-arrow-right(
          type='button',
          @click='(leftYear < rightYear - 1 || !isInterval) && leftYear++'
        )
      .box-choose
        .choose-list
          .choose-item(
            v-for='(item, index) in typeDictionary[dateType]',
            :key='index',
            :class='{ "choose-item-active": periodDataCopy instanceof Array ? periodDataCopy.some((element) => element === `${leftYear + item.value}`) : periodDataCopy === leftYear + item.value }',
            @click='handlerItemClick(`${leftYear + item.value}`, `${leftYear}年${item.name}`)'
          ) {{ item.name }}
    //- 右侧
    .box-right(v-show='isInterval')
      .box-year
        button.el-picker-panel__icon-btn.el-icon-d-arrow-left(
          type='button',
          @click='rightYear > leftYear + 1 && rightYear--'
        )
        .left-year {{ rightYear }}
        button.el-picker-panel__icon-btn.el-icon-d-arrow-right(
          type='button',
          @click='rightYear++'
        )
      .box-choose
        .choose-list
          .choose-item(
            v-for='(item, index) in typeDictionary[dateType]',
            :key='index',
            :class='{ "choose-item-active": periodDataCopy instanceof Array ? periodDataCopy.some((element) => element === `${rightYear + item.value}`) : periodDataCopy === rightYear + item.value }',
            @click='handlerItemClick(`${rightYear + item.value}`, `${rightYear}年${item.name}`)'
          ) {{ item.name }}
</template>

<script>
import { differenceWith, isEqual, findIndex } from 'lodash-es'

export default {
  name: 'QuarterOrHalfyear',
  model: {
    prop: 'periodData',
    event: 'change'
  },
  props: {
    // 默认的时间类型,可设置为 quarter / halfyear
    dateType: {
      type: String,
      default: 'quarter'
    },
    // 是否是区间
    isInterval: {
      type: Boolean,
      default: true
    },
    // 绑定的数据字段
    periodData: {
      type: [Array, String],
      default: () => []
    }
  },
  data() {
    return {
      // 时间类型字典
      typeDictionary: {
        quarter: [
          {
            name: '第一季度',
            value: 'Q1'
          },
          {
            name: '第二季度',
            value: 'Q2'
          },
          {
            name: '第三季度',
            value: 'Q3'
          },
          {
            name: '第四季度',
            value: 'Q4'
          }
        ],
        halfyear: [
          {
            name: '上半年',
            value: 'H1'
          },
          {
            name: '下半年',
            value: 'H2'
          }
        ]
      },
      // 左右两侧的年份
      leftYear: '',
      rightYear: '',
      // 备份数据,为了可以还原数据
      periodDataCopy: [],
      // 储存两次点击时所选中的数据
      saveClickData: [],
      // 是否显示时间选择框
      showChooseBox: false,
      // 输入框中回显的值
      showName: ''
    }
  },
  watch: {
    // 监测该值,为了解决外部修改数据,组件内部无法同步的问题
    periodData: {
      handler(newVal, oldVal) {
        // 值不一样的话,才做处理,非区间的字符串比较,和区间的数组比较
        if (
          newVal &&
          (oldVal !== newVal || differenceWith(oldVal, newVal, isEqual).length)
        ) {
          const { isInterval } = this
          if (isInterval) {
            // 区间
            let result = ''
            newVal.forEach((element, index) => {
              result += this.transformQuarterOrHalfyear(element)
              index === 0 && (result += ' - ')
            })
            this.showName = result
          } else {
            // 非区间
            this.showName = this.transformQuarterOrHalfyear(newVal)
          }
          this.periodDataCopy = newVal
        }
      },
      immediate: true
    }
  },
  mounted() {
    const year = this.$moment().format('YYYY')
    this.leftYear = year
    this.rightYear = year * 1 + 1
    // 如果没有默认时间 或者 需要默认回显文本,需要设置一下,为当前年份 + 当前季度 / 上/下半年
    const { periodData, typeDictionary, dateType, isInterval } = this
    if (!periodData || (periodData && !periodData.length)) {
      // 依据时间类型判断当前是哪一季度,或者是上半年还是下半年
      // 定义一个在 typeDictionary 字典中的索引
      let accurate
      if (dateType === 'quarter') {
        accurate = this.$moment().quarter() - 1
      } else {
        const month = this.$moment().month()
        month > 6 ? (accurate = 1) : (accurate = 0)
      }
      accurate = typeDictionary[dateType][accurate]
      const dateValue = `${year + accurate.value}`
      const dateName = `${year}年${accurate.name}`
      // 区间与非区间
      if (isInterval) {
        this.$emit('change', [dateValue, dateValue])
      } else {
        this.$emit('change', dateValue)
      }
    }
  },
  methods: {
    /**
     * 时间选择框 中底部的选项的点击逻辑
     *  vaue 2021Q1
     *  name 2021年第一季度
     */
    handlerItemClick(value, name) {
      const { periodDataCopy, isInterval } = this
      // 区间与非区间
      if (isInterval) {
        // 先判断,如果点击之前左右两侧都存在值,需要清空选项值,这里清空的只是备份值
        if (periodDataCopy && periodDataCopy.length === 2) {
          this.periodDataCopy = []
          this.saveClickData = []
        }
        // 这里如果是第二个选项,且 value 小于第一个选项,需要把它放到前面,否则正常追加
        if (
          this.saveClickData.length === 1 &&
          value < this.saveClickData[0].value
        ) {
          this.periodDataCopy.unshift(value)
          this.saveClickData.unshift({
            value,
            name
          })
        } else {
          this.periodDataCopy.push(value)
          this.saveClickData.push({
            value,
            name
          })
        }
        // 如果选中了两项数据,就直接赋值,关闭弹窗
        if (this.periodDataCopy.length === 2) {
          this.$emit('change', periodDataCopy)
          this.showChooseBox = false
        }
      } else {
        this.$emit('change', value)
        this.showChooseBox = false
      }
    },
    /**
     * 点击空白区域回调
     */
    handlerCancel() {
      this.showChooseBox = false
      const { periodDataCopy, isInterval } = this
      // 区间与非区间
      if (isInterval) {
        // 如果没有两个选中值,需要重置选择
        if (periodDataCopy && periodDataCopy.length !== 2) {
          this.periodDataCopy = this.periodData
          this.saveClickData = []
        }
      } else {
        // 非区间的
      }
    },
    /**
     * 封装一个 value 转成 name 的函数,示例:2021Q1 => 2021年第一季度;2021H2 => 2021年xiabann
     */
    transformQuarterOrHalfyear(value) {
      // 非空且格式正确
      if (value && value.length === 6) {
        const { typeDictionary } = this
        const newTypeDictionary = [
          ...typeDictionary.quarter,
          ...typeDictionary.halfyear
        ]
        const year = `${value.substr(0, 4)}年`
        let accurate = value.substr(4)
        const index = findIndex(
          newTypeDictionary,
          (element) => element.value === accurate
        )
        accurate = newTypeDictionary[index].name
        return `${year + accurate}`
      } else {
        return ''
      }
    }
  }
}
</script>

<style lang="stylus" scoped>
.quarter-or-halfyear
  position relative
  .mask-block
    position fixed
    top 0
    bottom 0
    left 0
    right 0
    margin auto
    z-index 996
  .date-choose-box
    box-shadow 0 0 0 1px $main-border-gray inset
    position absolute
    z-index 999
    top 40px
    width 500px
    height 250px
    background-color #fff
    display flex
    &-shot
      width 250px
    &>div:first-child
      box-shadow 1px 0 0 0 $main-border-gray
    &>div
      flex 1
      width 250px
      padding 16px
      .box-year
        line-height 28px
        margin 0 0 10px 0
        display flex
        justify-content space-between
        align-items center
        font-size 16px
        .el-picker-panel__icon-btn
          margin 0
      .box-choose
        .choose-list
          .choose-item
            text-align center
            line-height 24px
            font-size 12px
            cursor pointer
            margin 0 0 10px 0
            border-radius 8px
            &:hover, &-active
              background-color $main-blue
              color #fff
</style>

上面代码引用的外部依赖有:lodash-es moment

因为我们的业务需求,需要拼接数据格式为 2021Q1 2021H1,所以 时间类型字典 typeDictionary 是源码中这样定义的,这个可以根据业务需求自行更改。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260