基于vue2和element-ui的季度选择器思路

1,834 阅读5分钟

element-ui的日期选择器只有年月日的选项,假如需要添加季度选择器,应该如何设计

总体效果

image.png

输入框结构

<div
  class="
    el-date-editor el-range-editor
    el-input__inner
    el-date-editor--monthrange
    quarterPickerInput
  "
  :class="{ 'is-active': showPicker }"
  ref="input"
  @click="showPicker = true"
  @mouseover="showClaerIcon = true"
  @mouseout="showClaerIcon = false"
>
  <i class="el-input__icon el-range__icon el-icon-date"></i
  ><input
    autocomplete="off"
    placeholder="开始季度"
    name=""
    :value="startQuarter"
    class="el-range-input"
  /><span class="el-range-separator">至</span
  ><input
    autocomplete="off"
    placeholder="结束季度"
    name=""
    :value="endQuarter"
    class="el-range-input"
  /><i
    :style="{
      visibility: showClaerIcon && startQuarter ? 'visible' : 'hidden',
    }"
    class="el-range__close-icon el-icon-circle-close"
    @click.stop="clearValue"
  ></i>
</div>

image.png

先写输入框的结构,主要是 两个input,一个日期icon,一个清除icon,样式基于element的样式

弹出框结构

<transition name="el-zoom-in-top">
  <div
    v-if="showPicker"
    class="el-picker-panel el-date-range-picker el-popper transition-box"
    x-placement="top-start"
    @click.stop
  >
    <div class="el-picker-panel__body-wrapper">
      <div class="el-picker-panel__body">
        <div
          class="
            el-picker-panel__content
            el-date-range-picker__content
            is-left
          "
        >
          <div class="el-date-range-picker__header">
            <button
              type="button"
              class="el-picker-panel__icon-btn el-icon-d-arrow-left"
              @click="selectYear--"
            ></button>
            <div>{{ selectYear - 1 }} 年</div>
          </div>
          <ul class="quarter-list">
            <li
              v-for="item in quarterList"
              :key="item.key"
              @mouseover="handleMouseoverQuarter(0, item.key)"
              @click="handleClickQuarter(0, item.key)"
              :class="{
                min: isMinCurrentQuart(selectYear - 1, item.key),
                max: isMaxCurrentQuart(selectYear - 1, item.key),
                disabled:
                  !isOptionalQuart(selectYear - 1, item.key) ||
                  isOverOptionalNumber(selectYear - 1, item.key),
                active: isSelectedQuart(selectYear - 1, item.key),
                current: isCurrentQuart(selectYear - 1, item.key),
                passing: isPassingQuart(selectYear - 1, item.key),
              }"
            >
              <div>
                <span>{{ item.value }}</span>
              </div>
            </li>
          </ul>
        </div>
        <div
          class="
            el-picker-panel__content
            el-date-range-picker__content
            is-right
          "
        >
          <div class="el-date-range-picker__header">
            <button
              type="button"
              class="el-picker-panel__icon-btn el-icon-d-arrow-right"
              @click="selectYear++"
            ></button>
            <div>{{ selectYear }} 年</div>
          </div>
          <ul class="quarter-list">
            <li
              v-for="item in quarterList"
              :key="item.key"
              @mouseover="handleMouseoverQuarter(1, item.key)"
              @click="handleClickQuarter(1, item.key)"
              :class="{
                min: isMinCurrentQuart(selectYear, item.key),
                max: isMaxCurrentQuart(selectYear, item.key),
                disabled:
                  !isOptionalQuart(selectYear, item.key) ||
                  isOverOptionalNumber(selectYear, item.key),
                active: isSelectedQuart(selectYear, item.key),
                current: isCurrentQuart(selectYear, item.key),
                passing: isPassingQuart(selectYear, item.key),
              }"
            >
              <div>
                <span> {{ item.value }} </span>
              </div>
            </li>
          </ul>
        </div>
      </div>
    </div>
  </div>
</transition>

image.png

transition过渡动画,两个显示选择的季度框,头部复用element的样式,主体样式需要重新写

主体部分由于功能较多,如不需要可以考虑简化

数据设计

data() {
    return {
      optionalRangeQuarter: [],
      showClaerIcon: false,
      showPicker: false,
      selectedQuarter: [
        // {
        //   year: 2021,
        //   quarter: 3,
        // },
        // {
        //   year: 2021,
        //   quarter: 4,
        // },
      ], //确定了的数据
      selectedFirstQuarter: null, //点击选择了第一个
      startQuarter: "",
      endQuarter: "",
      currentQuarter: [], //在页面上选择,但未确定的数据
      selectYear: new Date().getFullYear(), //页面上框里右边显示的年份,左边就-1
      quarterList: [
        {
          key: 1,
          value: "第一季度",
          subname: "Q1",
          months: [1, 2, 3],
        },
        {
          key: 2,
          value: "第二季度",
          subname: "Q2",
          months: [4, 5, 6],
        },
        {
          key: 3,
          value: "第三季度",
          subname: "Q3",
          months: [7, 8, 9],
        },
        {
          key: 4,
          value: "第四季度",
          subname: "Q4",
          months: [10, 11, 12],
        },
      ],
    };
  },

主要数据:

  1. 四个季度的列表 quarterList
  2. 弹出框头部显示的年 selectYear,也就是头部的2021 2022,用其中一个年去表示,另一个加1或减1
  3. 已选择的季度数据 selectedQuarter 只选取最前和最后的那个季度
  4. 其他数据,鼠标经过显示灰色背景的项数据,不可选的项显示深灰色背景的数据 image.png

绑定和解绑弹出框弹出事件

mounted() {
    // 绑定页面点击事件
    document.addEventListener("click", this.bindClickEventListener);
},
destroyed() {
    // 解除绑定页面点击事件
    document.removeEventListener("click", this.bindClickEventListener);
},

用一个Boolean数据表示弹出框是否弹出,外层需要添加transition动画,点击时弹出

如果需要绑定多个事件,可使用闭包

格式转换方法

由于选择的前后季度,需要转为字符串数据,需要一个方法为对象和字符串数据相互转换

["2021-Q1","2022-Q1"]
//转换为
[{
    year: 2021,
    quarter: 1
},{
    year: 2022,
    quarter: 1
}]
formatQuarter(value) {
  //将字符串转成对象数组的数据结构
  return value.map((item) => {
    return {
      year: +item.split("-")[0],
      quarter: this.quarterList.find(
        (item2) => item2.subname == item.split("-")[1]
      ).key,
    };
  });
},

数据顺序排列方法

由于防止传入的数据不是从小到大排列的季度

或者点击时,先选择了后面的季度,再去选择前面的季度,需要一个方法对数据进行顺序处理

changeOrder(value) {
  //改变数组的项顺序,从小到大
  if (!(value instanceof Array) || value.length < 2) return value;
  let formatValue = [...value];
  if (typeof value[0] === "string") {
    formatValue = this.formatQuarter(value);
  }
  if (formatValue[0].year > formatValue[1].year) {
    [value[0], value[1]] = [value[1], value[0]];
  } else if (
    formatValue[0].year == formatValue[1].year &&
    formatValue[0].quarter > formatValue[1].quarter
  ) {
    [value[0], value[1]] = [value[1], value[0]];
  }
  return [...value];
},

初始化默认季度数据

setDefaultValue() {
  //将传进来的默认value转成组件数据结构显示在页面
  let defaultValue = this.changeOrder(this.defaultValue);
  this.selectedQuarter = this.formatQuarter(defaultValue);
},

将传入的默认初始化显示,用格式化方法将父组件传入的字符串数据处理成页面显示的数据

鼠标经过时,显示灰色背景

鼠标经过的数据需要显示灰色背景,需要一个方法将鼠标经过时所经过的那个季度和已选择的第一个季度进行对比大小,然后处理成一个长度为2的数组数据,表示最前和最后一个季度

handleMouseoverQuarter(type, quarter) {
  //type为 0 左边框,type: 1 右边框
  let year = type ? this.selectYear : this.selectYear - 1;
  if (!this.checkOptionalQuart(year, quarter)) return;
  if (this.compareSelectedFirstQuarter(year, quarter)) return;
  if (this.selectedFirstQuarter) {
    this.currentQuarter.length = 1;
    this.currentQuarter.push({
      year,
      quarter,
    });
  }
},

鼠标点击时,选择季度

鼠标点击时选择季度,将选择的季度排序,如果是第一次选择,直接存进数组第一项,如果是第二次选泽,将数组排序后存进数组,这时需要一个data数据代表是否第一次选择

handleClickQuarter(type, quarter) {
  //type为 0 左边框,type: 1 右边框
  let year = type ? this.selectYear : this.selectYear - 1;
  if (!this.checkOptionalQuart(year, quarter)) return;
  if (this.compareSelectedFirstQuarter(year, quarter)) return;
  if (
    this.selectedFirstQuarter == null &&
    this.currentQuarter.length >= 2
  ) {
    this.currentQuarter.length = 0;
  }
  if (this.currentQuarter.length < 2) {
    this.selectedFirstQuarter = {
      year,
      quarter,
    };
    this.currentQuarter.push(this.selectedFirstQuarter);
  }
  if (this.currentQuarter.length >= 2) {
    let selectedQuarter = [...this.currentQuarter];
    this.selectedQuarter = this.changeOrder(selectedQuarter);
    this.showPicker = false;
  }
},

如果以上的操作中,鼠标经过或鼠标点击,第一次和第二次的季度数据是同一个,将不做处理,需要一个方法去判断第二次所选的和第一次所选的是否同一个数据

compareSelectedFirstQuarter(year, quarter) {
  if (this.selectedFirstQuarter) {
    let arr = this.changeOrder([
      { ...this.selectedFirstQuarter },
      { year, quarter },
    ]);
    let difference =
      4 * (arr[1].year - arr[0].year) +
      (arr[1].quarter - arr[0].quarter + 1);
    if (difference > this.optionalNumber) {
      return true;
    }
  }
  return false;
},

可选区间或不可选区间

由于需求中,可选区间比不可选区间更容易处理,所以选择了做 判断是否可选区间,如果是不可选区间,将以上鼠标移入和点击操作return中止

checkOptionalQuart(year, quarter) {
  //验证是否在可选择区间的季度
  if (
    !this.optionalRangeQuarter.length ||
    year < this.optionalRangeQuarter[0].year ||
    year > this.optionalRangeQuarter[1].year
  ) {
    return false;
  } else if (
    year == this.optionalRangeQuarter[0].year &&
    quarter < this.optionalRangeQuarter[0].quarter
  ) {
    return false;
  }
  if (
    year == this.optionalRangeQuarter[1].year &&
    quarter > this.optionalRangeQuarter[1].quarter
  ) {
    return false;
  }
  return true;
},

处理页面数据样式

通过以上的方法,即可将已选择、鼠标经过、禁用、最小、最大等数据处理好,只要适用computed将数据计算此季度应显示怎样的样式,以下为其中一种计算最小的已选属性,其他计算类似

isMinCurrentQuart() {
  return (year, quarter) => {
    if (this.currentQuarter.length >= 2) {
      let currentQuarter = this.changeOrder([...this.currentQuarter]);
      return (
        currentQuarter[0].year == year &&
        currentQuarter[0].quarter == quarter
      );
    } else if (this.currentQuarter.length == 1) {
      return true;
    }
  };
},

数据通知父组件

最后,将已选择的季度数据,处理成字符串的形式,在watch中通过自定义事件返回给父组件

父组件调用

<QuarterPicker
  :defaultValue="['2021-Q3', '2021-Q3']"
  :optionalRange="['2021-Q1', '2022-Q4']"
  :optionalNumber="4"
  @change="changeQuarterValue"
/>

父组件调用时,包括默认数据,可选区域,可选连续季度数,change事件

此处未进行双向绑定,如果需要双向绑定的话,可以使用v-model进行双向绑定value

使用时更方便,但可能不够灵活,不建议这样做