element-ui年份范圍選擇器

故事背景: 接著上次自定義單行文本輸入框需求, 還有一個(gè)自定義日期的需求, 需要支持選擇年, 年月, 年月日, 年月日時(shí), 年月日時(shí)分, 同時(shí)還要支持上面類(lèi)型日期區(qū)間選擇庸毫。這個(gè)需求的難點(diǎn)在于其他所有類(lèi)型 element-ui 都有對(duì)應(yīng)的組件, 但是唯獨(dú)年份范圍選擇器 element-ui 沒(méi)有支持,咱也不知道為啥餓了嗎團(tuán)隊(duì)沒(méi)有寫(xiě)! 產(chǎn)品現(xiàn)在需要這樣的功能, 我們總不至于說(shuō)第三方庫(kù)不支持吧, 老臉擱不下, 同時(shí)之前也有被小伙伴問(wèn)過(guò)怎么自己實(shí)現(xiàn)一個(gè)類(lèi)似 element-ui 年份區(qū)間的選擇器, 花了半天時(shí)間, 我們也勉勉強(qiáng)強(qiáng)實(shí)現(xiàn)了這個(gè)需求, 趁著五一稍微閑點(diǎn), 我們把代碼整理了下,現(xiàn)在貼出來(lái)供大家查考蚌父。后期會(huì)考慮放到 npm 上, 供大家更方便使用舌缤。
配置日期選擇器頁(yè)面如下:

image.png

我們實(shí)現(xiàn)效果如下:
Kapture 2022-05-01 at 01.57.51.gif

使用注意事項(xiàng):

  • 此組件基于 element-ui 組件 & 樣式(el-popover & el-date-editor 樣式), 所以項(xiàng)目ui 框架必須是 element-ui
  • 組件功能目前還比較弱, v-model只支持綁定 Array 類(lèi)型, 一些事件也不支持, 本人后期會(huì)根據(jù)閱讀量和讀者反饋加入一些需要功能
下面直接上代碼塊, 直接 copy 就可使用。
<template>
  <el-popover
    ref="popover"
    placement="bottom"
    v-model="showPanel"
    popper-class="custom_year_range"
    trigger="manual"
    v-clickoutside="() => { showPanel = false }"
  >
    <div class="_inner floatPanel">
      <div class="_inner leftPanel">
        <div class="_inner panelHead">
          <i
            class="_inner el-icon-d-arrow-left"
            @click="onClickLeft"
          ></i>
          <span>
            {{ leftYearList[0] + '年 ' + '- ' + leftYearList[9] + '年' }}
          </span>
        </div>
        <div class="_inner panelContent">
          <div
            :class="{
              oneSelected: item === startYear && oneSelected,
              startSelected: item === startYear,
              endSelected: item === endYear,
              betweenSelected: item > startYear && item < endYear,
            }"
            v-for="item in leftYearList"
            :key="item"
          >
            <a
              :class="{
                cell: true,
                _inner: true,
                selected: item === startYear || item === endYear,
              }"
              @click="onClickItem(item)"
              @mouseover="onHoverItem(item)"
            >
              {{ item }}
            </a>
          </div>
        </div>
      </div>
      <div class="_inner rightPanel">
        <div class="_inner panelHead">
          <i
            class="_inner el-icon-d-arrow-right"
            @click="onClickRight"
          ></i>
          <span>{{ rightYearList[0] + '年 ' + '- ' + rightYearList[9] + '年' }}</span>
        </div>
        <div class="_inner panelContent">
          <div
            :class="{
              startSelected: item === startYear,
              endSelected: item === endYear,
              betweenSelected: item > startYear && item < endYear,
            }"
            v-for="item in rightYearList"
            :key="item"
          >
            <a
              :class="{
                cell: true,
                _inner: true,
                selected: item === endYear || item === startYear,
              }"
              @click="onClickItem(item)"
              @mouseover="onHoverItem(item)"
            >
              {{ item }}
            </a>
          </div>
        </div>
      </div>
    </div>
    <div slot="reference">
      <div
        ref="yearPicker"
        style="width: 100%"
        class="el-date-editor el-range-editor el-input__inner el-date-editor--daterange el-range-editor--small"
      >
        <i class="el-input__icon el-range__icon el-icon-date"></i>
        <input
          class="_inner range_input"
          ref="inputLeft"
          type="text"
          name="yearInput"
          placeholder="選擇開(kāi)始年份"
          v-model="startShowYear"
          @focus="onFocus"
          @keyup="handleInput('start')"
        />
        <span class="el-range-separator">{{ sp }}</span>
        <input
          class="_inner range_input"
          ref="inputRight"
          type="text"
          name="yearInput"
          placeholder="選擇結(jié)束年份"
          v-model="endShowYear"
          @focus="onFocus"
          @keyup="handleInput('end')"
        />
      </div>
    </div>
  </el-popover>
</template>

<script>
import moment from 'moment'
import { clickoutside, SELECT_STATE } from '@/views/customerLists/components/custom/customView/components/utils.js'
export default {
  name: 'yearPicker',
  directives: { clickoutside },
  computed: {
    oneSelected() {
      return this.curState === SELECT_STATE.selecting && (this.startYear === this.endYear || this.endYear == null)
    },
    leftYearList() {
      return this.yearList.slice(0, 10)
    },
    rightYearList() {
      return this.yearList.slice(10, 20)
    }
  },
  props: {
    sp: {
      default: '至'
    },
    value: {
      default: null
    }
  },
  data() {
    return {
      itemBg: {},
      startShowYear: null,
      endShowYear: null,
      yearList: [],
      showPanel: false,
      startYear: null,
      endYear: null,
      curYear: 0,
      curSelectedYear: 0,
      curState: SELECT_STATE.unselect
    }
  },
  methods: {
    handleInput(type) {
      switch (type) {
        case 'start':
          if (isNaN(this.startShowYear)) {
            this.startShowYear = this.startYear
            return
          }
          this.startYear = this.startShowYear * 1
          break
        case 'end':
          if (isNaN(this.endShowYear)) {
            this.endShowYear = this.endYear
            return
          }
          this.endYear = this.endShowYear * 1
          break
      }
      [this.startYear, this.endYear] = [this.endYear, this.startYear]
      this.startShowYear = this.startYear
      this.endShowYear = this.endYear
    },

    onHoverItem(iYear) {
      if (this.curState === SELECT_STATE.selecting) {
        const tmpStart = this.curSelectedYear
        this.endYear = Math.max(tmpStart, iYear)
        this.startYear = Math.min(tmpStart, iYear)
      }
    },

    async onClickItem(selectYear) {
      if (
        this.curState === SELECT_STATE.unselect ||
        this.curState === SELECT_STATE.selected
      ) {
        this.startYear = selectYear
        this.curSelectedYear = selectYear
        this.endYear = null
        this.curState = SELECT_STATE.selecting
      } else if (this.curState === SELECT_STATE.selecting) {
        this.endShowYear = this.endYear || this.startYear
        this.startShowYear = this.startYear
        this.curState = SELECT_STATE.selected
        await this.$nextTick()
        this.showPanel = false
        this.$parent?.$parent?.$parent?.$parent?.$parent.clearValidate?.()
      }
    },

    async onFocus() {
      await this.$nextTick()
      this.showPanel = true
    },

    updateYearList() {
      const startYear = ~~(this.curYear / 10) * 10
      console.log(startYear, this.curYear, 'this.curYearthis.curYearthis.curYear')
      this.yearList = []
      for (let index = 0; index < 20; index++) {
        this.yearList.push(startYear + index)
      }
    },

    onClickLeft() {
      this.curYear = this.curYear * 1 - 10
      this.updateYearList()
    },

    onClickRight() {
      this.curYear = this.curYear * 1 + 10
      this.updateYearList()
    }
  },
  watch: {
    value: {
      handler(val) {
        if (val?.length === 0) return
        const [first, end] = val || []
        this.startShowYear = first
        this.endShowYear = end
      },
      immediate: true,
      deep: true
    },

    startShowYear: {
      handler(val) {
        this.$emit('input', [val, this.endShowYear || ''])
      },
      immediate: true,
      deep: true
    },

    endShowYear: {
      handler(val) {
        this.$emit('input', [this.startShowYear || '', val])
      },
      immediate: true,
      deep: true
    }
  },
  created() {
    const [startYear, endYear] = this.value || []
    if (startYear) {
      this.startYear = Number(startYear)
      this.endYear = Number(endYear)
      this.curState = SELECT_STATE.selected
      this.curYear = startYear
    } else {
      this.curYear = moment().format('yyyy')
    }
    this.updateYearList()
  },

  mounted() {
    window.Vue = this
  }
}
</script>
<style lang="scss">
.custom_year_range {
  .floatPanel {
    > div {
      width: 50%;
    }
    padding: 0 16px;
    // position: absolute;
    display: flex;
    background-color: #fff;
    z-index: 2000;
    border-radius: 4px;
    width: 650px;
    height: 250px;
    top: 40px;
    left: -50px;
    .panelContent {
      display: flex;
      flex-wrap: wrap;
      width: 100%;
      height: calc(100% - 70px);
      .oneSelected {
        border-top-right-radius: 24px;
        border-bottom-right-radius: 24px;
      }
      .startSelected {
        background-color: #f2f6fc;
        border-top-left-radius: 24px;
        border-bottom-left-radius: 24px;
      }
      .endSelected {
        background-color: #f2f6fc;
        border-top-right-radius: 24px;
        border-bottom-right-radius: 24px;
      }
      .betweenSelected {
        background-color: #f2f6fc;
      }
      > div {
        width: 75px;
        height: 48px;
        line-height: 48px;
        margin: 3px 0;
        // border-radius: 24px;
        text-align: center;
        a {
          display: inline-block;
          width: 60px;
          height: 36px;
          cursor: pointer;
          line-height: 36px;
          border-radius: 18px;
          &:hover {
            color: #409eff;
          }
        }
        .selected {
          background-color: #409eff;
          color: #fff;
          &:hover {
            color: #fff !important;
          }
        }
      }
    }
    .panelHead {
      position: relative;
      height: 46px;
      line-height: 46px;
      text-align: center;
      display: flex;
      align-items: center;
      justify-content: center;
      span {
        font-size: 16px;
        font-weight: 500;
        padding: 0 5px;
        line-height: 22px;
        text-align: center;
        cursor: pointer;
        color: #606266;
        &:hover {
          color: #409eff;
        }
      }
      i {
        position: absolute;
        cursor: pointer;
        &:hover {
          color: #3e77fc;
        }
      }
    }
    .rightPanel {
      padding-left: 8px;
    }
    .leftPanel .panelHead i {
      left: 20px;
    }
    .rightPanel .panelHead i {
      right: 20px;
    }
  }
  .floatPanel::before {
    content: "";
    height: 100%;
    top: 0;
    position: absolute;
    left: 50%;
    width: 1px;
    border-left: 1px solid #e4e4e4;
  }
}
</style>
<style lang="scss" scoped>
.range_input {
  appearance: none;
  border: none;
  outline: 0;
  padding: 0;
  width: 39%;
  color: #606266;
  line-height: 1;
  height: 100%;
  margin: 0;
  text-align: center;
  display: inline-block;
}
.yearPicker {
  // font-size: 14px;
  // display: flex;
  // position: relative;
  // transition: all 0.3s;
  input:first-child {
    text-align: right;
  }
  .labelText {
    position: absolute;
    left: 8px;
  }
  background-color: #fff;
  span {
    padding: 0 8px;
    height: 32px;
    line-height: 32px;
  }
  border: 1px solid #eff1f3;
  height: 34px;
  line-height: 34px;
  border-radius: 4px;
  padding: 0 28px 0 8px;
  box-sizing: border-box;
}
input {
  width: 60px;
  border: none;
  height: 32px;
  line-height: 32px;
  box-sizing: border-box;
  background-color: transparent;
}
input:focus {
  outline: none;
  background-color: transparent;
}
.yearPicker:hover {
  border-color: #3e77fc;
}
.dateIcon {
  position: absolute;
  right: 16px;
  top: 9px;
  color: #adb2bc;
}
</style>
<!-- @/views/customerLists/components/custom/customView/components/utils.js -->
export const clickoutside = {
  bind(el, binding, vnode) {
    function documentHandler(e) {
      // 這里判斷點(diǎn)擊的元素是否是本身,是本身,則返回
      if (el.contains(e.target)) {
        return false
      }
      // 判斷指令中是否綁定了函數(shù)
      if (binding && binding.expression) {
        // 如果綁定了函數(shù) 則調(diào)用那個(gè)函數(shù)谱姓,此處binding.value就是handleClose方法
        if (binding.value && binding.value(e)) {
          binding.value(e)
        }
      }
    }
    // 給當(dāng)前元素綁定個(gè)私有變量,方便在unbind中可以解除事件監(jiān)聽(tīng)
    el.__vueClickOutside__ = documentHandler
    document.addEventListener('click', documentHandler)
  },
  unbind(el, binding) {
    // 解除事件監(jiān)聽(tīng)
    document.removeEventListener('click', el.__vueClickOutside__)
    delete el.__vueClickOutside__
  }
}

export const SELECT_STATE = {
  unselect: 0,
  selecting: 1,
  selected: 2
}

如果用到了,希望給個(gè)贊, 小弟在這感激不盡

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末刨晴,一起剝皮案震驚了整個(gè)濱河市屉来,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌狈癞,老刑警劉巖茄靠,帶你破解...
    沈念sama閱讀 222,000評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異蝶桶,居然都是意外死亡慨绳,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,745評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén)真竖,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)脐雪,“玉大人,你說(shuō)我怎么就攤上這事疼邀∥菇” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,561評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵旁振,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我涨岁,道長(zhǎng)拐袜,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,782評(píng)論 1 298
  • 正文 為了忘掉前任梢薪,我火速辦了婚禮蹬铺,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘秉撇。我一直安慰自己甜攀,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,798評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布琐馆。 她就那樣靜靜地躺著规阀,像睡著了一般。 火紅的嫁衣襯著肌膚如雪瘦麸。 梳的紋絲不亂的頭發(fā)上谁撼,一...
    開(kāi)封第一講書(shū)人閱讀 52,394評(píng)論 1 310
  • 那天,我揣著相機(jī)與錄音滋饲,去河邊找鬼厉碟。 笑死喊巍,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的箍鼓。 我是一名探鬼主播崭参,決...
    沈念sama閱讀 40,952評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼款咖!你這毒婦竟也來(lái)了何暮?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,852評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤之剧,失蹤者是張志新(化名)和其女友劉穎郭卫,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體背稼,經(jīng)...
    沈念sama閱讀 46,409評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡贰军,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,483評(píng)論 3 341
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了蟹肘。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片词疼。...
    茶點(diǎn)故事閱讀 40,615評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖帘腹,靈堂內(nèi)的尸體忽然破棺而出贰盗,到底是詐尸還是另有隱情,我是刑警寧澤阳欲,帶...
    沈念sama閱讀 36,303評(píng)論 5 350
  • 正文 年R本政府宣布舵盈,位于F島的核電站,受9級(jí)特大地震影響球化,放射性物質(zhì)發(fā)生泄漏秽晚。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,979評(píng)論 3 334
  • 文/蒙蒙 一筒愚、第九天 我趴在偏房一處隱蔽的房頂上張望赴蝇。 院中可真熱鬧,春花似錦巢掺、人聲如沸句伶。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,470評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)考余。三九已至,卻和暖如春倔约,著一層夾襖步出監(jiān)牢的瞬間秃殉,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,571評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留钾军,地道東北人鳄袍。 一個(gè)月前我還...
    沈念sama閱讀 49,041評(píng)論 3 377
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像吏恭,于是被迫代替她去往敵國(guó)和親拗小。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,630評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容