Vue2 个人组件库 —— 日期选择器

691 阅读1分钟

个人组件库文档地址

DatePicker 日期选择器

基础用法

<vp-date-picker v-model="datePicker1" placeholder="请选择日期" />

<script>
  export default {
    data() {
      return {
        datePicker1: "",
      };
    },
  };
</script>

不可选中

<vp-date-picker
  v-model="datePicker2"
  placeholder="请选择日期"
  :picker-option="pickerOption"
  @blur="handleDatePickerBlur"
  @focus="handleDatePickerFocus"
  @change="handleDatePickerChange"
/>

<script>
  export default {
    data() {
      return {
        datePicker2: "",
        pickerOption: {
          disabledDate(time) {
            return time.getTime() > Date.now();
          },
        },
      };
    },
    methods: {
      handleDatePickerBlur(vm) {
        console.log(vm);
      },
      handleDatePickerFocus(vm) {
        console.log(vm);
      },
      handleDatePickerChange(vm) {
        console.log(vm);
      },
    }
  };
</script>

DatePicker 组件代码

<template>
  <div class="vp-date-picker">
    <vp-input
      v-model="date"
      :placeholder="placeholder"
      @focus="handleInputFocus"
      @blur="handleInputBlur($event)"
    >
      <template v-slot:prefix>
        <span class="iconfont icon-rili"></span>
      </template>
    </vp-input>

    <!-- 弹出层 -->
    <transition name="slide-fade">
      <div :class="['vp-date-picker-container']" v-show="active">
        <!-- 当前日期 -->
        <div class="vp-date-picker_control">
          <!-- 左 -->
          <div class="date_left">
            <div class="date_pre_year" @click="handlePreYear">
              <span class="iconfont icon-jiantou_yemian_xiangzuo_o"></span>
            </div>
            <div class="date_pre_month" @click="handlePreMonth">
              <span class="iconfont icon-jiantou_liebiaoxiangzuo_o"></span>
            </div>
          </div>
          <!-- 内容 -->
          <div class="date_text">
            {{ currentTime.currentYear }} 年 {{ currentTime.currentMonth }} 月
          </div>
          <!-- 右 -->
          <div class="date_right">
            <div class="date_suffix_month" @click="handleSuffixMonth">
              <span class="iconfont icon-jiantou_liebiaoxiangyou_o"></span>
            </div>
            <div class="date_suffix_year" @click="handleSuffixYear">
              <span class="iconfont icon-jiantou_yemian_xiangyou_o"></span>
            </div>
          </div>
        </div>
        <!-- 日期 -->
        <table class="vp-date-table">
          <thead>
            <tr>
              <th v-for="week in weekList" :key="week">
                {{ week }}
              </th>
            </tr>
          </thead>
          <tbody>
            <template v-for="row in rowLength">
              <tr :key="row">
                <td
                  v-for="td in 7"
                  :key="td"
                  :class="[
                    'activable',
                    (row - 1) * 7 + td - 1 <= preLastIndex ? 'preMonth_td' : '',
                    (row - 1) * 7 + td - 1 >= suffixFirstIndex
                      ? 'suffixMonth_td'
                      : '',

                    currentTime.currentYear === nowYear &&
                    currentTime.currentMonth === nowMonth &&
                    dayList[(row - 1) * 7 + td - 1].day === nowDay &&
                    (row - 1) * 7 + td - 1 === currentDateIndex
                      ? 'currentTime__tb'
                      : '',
                  ]"
                  @click="
                    handleSelectDate(
                      dayList[(row - 1) * 7 + td - 1].day,
                      (row - 1) * 7 + td - 1,
                      $event
                    )
                  "
                >
                  <div
                    :class="[
                      pickerOption &&
                      pickerOption.disabledDate(
                        dayList[(row - 1) * 7 + td - 1].date
                      )
                        ? 'disabled_day'
                        : '',
                    ]"
                  >
                    <span
                      :class="[
                        'date_table_span',
                        currentTime.currentYear === selectYear &&
                        currentTime.currentMonth === selectMonth &&
                        dayList[(row - 1) * 7 + td - 1].day === selectDay &&
                        (row - 1) * 7 + td - 1 > preLastIndex &&
                        (row - 1) * 7 + td - 1 < suffixFirstIndex
                          ? 'select_time_tab'
                          : '',
                      ]"
                      >{{ dayList[(row - 1) * 7 + td - 1].day }}</span
                    >
                  </div>
                </td>
              </tr>
            </template>
          </tbody>
        </table>
      </div>
    </transition>
    <transition name="slide-fade">
      <div class="vp-option_san" v-show="active"></div>
    </transition>
  </div>
</template>

<script>
export default {
  name: "vpDatePicker",
  props: {
    // placeholder
    placeholder: {
      type: String,
      default: "",
    },
    // v-model value
    value: {
      type: Date | String,
    },
    // picker-option
    pickerOption: {
      type: Object,
      default: () => {},
    },
    // align
    align: {
      type: String,
      default: "left",
    },
  },
  watch: {
    // value
    value: {
      handler(newVal) {
        let date = new Date(newVal);
        let month = date.getMonth();
        let year = date.getFullYear();
        let day = date.getDate();
        let monthList = [1, 3, 5, 7, 8, 10, 12];
        if (month === 0 && day === 31) {
          month = 1;
        } else if (monthList.includes(month) && day === 31) {
          month += 1;
        } else if (!monthList.includes(month) && month !== 0 && day === 31) {
          month += 1;
        }
        this.date = `${year}-${month < 10 ? "0" + month : month}-${
          day < 10 ? "0" + day : day
        }`;
      },
    },

    // 当前日期变化
    currentTime: {
      handler(newVal) {
        this.getFullMonthDateList();
        // 当前选中索引
        for (let i = this.preLastIndex + 1; i < this.suffixFirstIndex; i++) {
          if (this.dayList[i] === this.selectDay) {
            this.selectDayIndex = i;
            break;
          }
        }
      },
      deep: true,
    },

    // picker-option
    pickerOption: {
      handler(newVal) {
        if (!newVal) return;
        // disabledDate
        console.log(newVal);
      },
      immediate: true,
      deep: true,
    },
  },
  data() {
    return {
      date: "",
      active: false,
      dayList: [],
      weekList: ["日", "一", "二", "三", "四", "五", "六"],
      currentTime: {
        currentYear: "",
        currentMonth: "",
        currentDate: "",
      },
      // 现在时间 day 索引
      currentDateIndex: "",
      rowLength: 6,
      preLastIndex: 0,
      suffixFirstIndex: 0,
      // 当前时间
      nowYear: new Date().getFullYear(),
      nowMonth: new Date().getMonth() + 1,
      nowDay: new Date().getDate(),
      selectYear: "",
      selectMonth: "",
      selectDay: "",
      selectDayIndex: "",
      // 当前点击是否在弹出层内部
      isClickContainer: false,
      // 选项
      option: {
        disabledDateFun: null,
      },
    };
  },
  created() {
    this.initPickerOption();
    this.initNowDate();
    this.getFullMonthDateList();
  },
  mounted() {
    this.handleMouseClick();
  },
  methods: {
    /**
     * 初始化 pickerOption
     */
    initPickerOption() {
    },

    /**
     * 初始化日期时间
     */
    initNowDate() {
      let date = new Date();
      this.currentTime.currentYear = date.getFullYear();
      this.currentTime.currentMonth = date.getMonth() + 1;
      this.currentTime.currentDate = date.getDate();
    },

    /**
     * 根据年月获取月第某一天星期几
     */
    getDateDay(year, month, day = 1) {
      let date = new Date(year, month - 1, day);
      return date.getDay();
    },

    /**
     * 获取 42 个日期列表
     */
    getFullMonthDateList() {
      let dayList = [];
      // 本月 1 号星期几
      let firstDay = this.getDateDay(
        this.currentTime.currentYear,
        this.currentTime.currentMonth,
        1
      );
      // 本月最后一天
      let lastDay = new Date(
        this.currentTime.currentYear,
        this.currentTime.currentMonth,
        0
      ).getDate();
      for (let i = 1; i <= lastDay; i++) {
        dayList.push({
          day: i,
          date: new Date(
            this.currentTime.currentYear,
            this.currentTime.currentMonth - 1,
            i
          ),
        });
      }
      // 在本月第一天之前还有多少天
      let preDay = firstDay !== 0 ? firstDay : 0;
      // 获取上一个月最后一天
      let preMonthLastDate = new Date(
        this.currentTime.currentYear,
        this.currentTime.currentMonth - 1,
        0
      ).getDate();
      this.preLastIndex = preDay - 1;
      for (let i = 0; i < preDay; i++) {
        dayList.unshift({
          day: preMonthLastDate - i,
          date: new Date(
            this.currentTime.currentMonth === 1
              ? this.currentTime.currentYear - 1
              : this.currentTime.currentYear,
            this.currentTime.currentMonth === 1
              ? 11
              : this.currentTime.currentMonth - 1 - 1,
            preMonthLastDate - i
          ),
        });
      }
      let suffixLength = 42 - dayList.length;
      this.suffixFirstIndex = 42 - (42 - dayList.length);
      // 补充下一月剩余日期
      for (let i = 1; i <= suffixLength; i++) {
        dayList.push({
          day: i,
          date: new Date(
            this.currentTime.currentMonth === 12
              ? this.currentTime.currentYear + 1
              : this.currentTime.currentYear,
            this.currentTime.currentMonth === 12
              ? 0
              : this.currentTime.currentMonth + 1 - 1,
            i
          ),
        });
      }
      this.currentDateIndex = dayList.findIndex(
        (item, index) =>
          index > this.preLastIndex &&
          index < this.suffixFirstIndex &&
          item.day === new Date().getDate()
      );
      this.dayList = dayList;
    },

    /**
     * input blur
     */
    handleInputBlur(e, event) {
      this.$emit("blur", this);
    },

    /**
     * input focus
     */
    handleInputFocus() {
      this.active = true;
      this.$emit("focus", this);
    },

    /**
     * 点击选中日期
     */
    handleSelectDate(day, index, event) {
      // 设置不可点击
      if (
        event.target.parentElement.className === "disabled_day" ||
        event.target?.lastElementChild?.className === "disabled_day"
      ) {
        return;
      }

      if (this.preLastIndex < index && index < this.suffixFirstIndex) {
        this.selectDayIndex = index;
        this.selectDay = day;
        this.selectYear = this.currentTime.currentYear;
        this.selectMonth = this.currentTime.currentMonth;
      } else if (index <= this.preLastIndex) {
        let currentMonth = this.currentTime.currentMonth;
        this.selectYear =
          currentMonth === 1
            ? this.currentTime.currentYear - 1
            : this.currentTime.currentYear;
        this.selectMonth =
          currentMonth - 1 < 1 ? 12 : this.currentTime.currentMonth - 1;
        this.selectDay = day;
        this.currentTime.currentMonth = this.selectMonth;
        this.currentTime.currentDate = day;
        this.currentTime.currentYear = this.selectYear;
      } else if (index >= this.suffixFirstIndex) {
        this.selectYear =
          this.currentTime.currentMonth === 12
            ? this.currentTime.currentYear + 1
            : this.currentTime.currentYear;
        this.selectMonth =
          this.currentTime.currentMonth === 12
            ? 1
            : this.currentTime.currentMonth + 1;
        this.currentTime.currentMonth =
          this.currentTime.currentMonth === 12
            ? 1
            : this.currentTime.currentMonth + 1;
        this.selectDay = day;
        this.currentTime.currentDate = day;
        this.currentTime.currentYear = this.selectYear;
      }
      let monthList = [1, 3, 5, 7, 8, 10, 12];
      let selectMonth = this.selectMonth;
      if (monthList.includes(this.selectMonth) && this.selectDay === 31) {
        selectMonth -= 1;
      }
      // 触发修改 v-model
      this.$emit(
        "input",
        new Date(this.selectYear, selectMonth, this.selectDay)
      );
      this.$emit(
        "change",
        new Date(this.selectYear, selectMonth, this.selectDay)
      );
      // 隐藏弹出层
      this.active = false;
    },

    /**
     * 上一年 pre_year
     */
    handlePreYear() {
      this.currentTime.currentYear = this.currentTime.currentYear - 1;
    },

    /**
     * 下一年 suffix_year
     */
    handleSuffixYear() {
      this.currentTime.currentYear = this.currentTime.currentYear + 1;
    },

    /**
     * 上一月 pre_month
     */
    handlePreMonth() {
      let currentMonth = this.currentTime.currentMonth;
      if (currentMonth === 1) {
        this.currentTime.currentMonth = 12;
        this.currentTime.currentYear = this.currentTime.currentYear - 1;
      } else {
        this.currentTime.currentMonth = currentMonth - 1;
      }
    },

    /**
     * 下一月 suffix_year
     */
    handleSuffixMonth() {
      this.currentTime.currentYear =
        this.currentTime.currentMonth + 1 > 12
          ? this.currentTime.currentYear + 1
          : this.currentTime.currentYear;
      this.currentTime.currentMonth =
        this.currentTime.currentMonth + 1 > 12
          ? 1
          : this.currentTime.currentMonth + 1;
    },

    /**
     * 鼠标当前点击位置
     */
    handleMouseClick() {
      document.addEventListener(
        "mouseup",
        (e) => {
          let flag = this.findParent(
            e.target,
            this.$el.querySelector(".vp-date-picker-container")
          );
          if (flag) {
            this.active = true;
            this.isClickContainer = true;
          } else if (e.target !== this.$el.querySelector(".vp-input-inner")) {
            this.active = false;
            this.isClickContainer = false;
          } else if (e.target === this.$el.querySelector(".vp-input-inner")) {
            this.active = true;
            this.isClickContainer = false;
          } 
        },
        true
      );
    },

    /**
     * 递归当前元素的父元素是否为弹出层
     */
    findParent(target, parent) {
      if (!target) {
        return false;
      }
      if (target === parent || target?.parentElement === parent) {
        return true;
      }
      return this.findParent(target.parentElement, parent);
    },
  },
};
</script>

<style lang="less" scoped>
.vp-date-picker {
  position: relative;
  display: inline-block;

  .vp-input {
    width: 220px;

    .vp-input_inner {
      &:hover {
        cursor: pointer;
      }
    }
  }

  .slide-fade-enter-active {
    transition: all 0.3s ease;
  }
  .slide-fade-leave-active {
    transition: all 0.3s cubic-bezier(1, 0.5, 0.8, 1);
  }
  .slide-fade-enter,
  .slide-fade-leave-to {
    opacity: 0;
  }

  .vp-date-picker-container {
    position: absolute;
    display: inline-block;
    box-sizing: border-box;
    // width: 100%;
    top: 135%;
    z-index: 9999;
    border: 1px solid #e4e7ed;
    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
    background-color: #fff;
    padding: 20px;

    .vp-date-picker_control {
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-bottom: 10px;

      .date_left,
      .date_right {
        display: flex;
        align-items: center;

        span {
          font-size: 24px;
          cursor: pointer;

          &:hover {
            color: #409eff;
          }
        }
      }
    }

    .vp-option-container_inner {
      max-height: 204px;
      overflow-x: hidden;
    }

    .vp-date-table {
      font-size: 12px;
      th {
        padding: 5px;
        color: #606266;
        font-weight: 400;
        border: none;
        border-bottom: 1px solid #ebeef5;
      }

      .suffixMonth_td,
      .preMonth_td {
        color: #c0c4cc;
      }

      td {
        padding: 4 px 0;
        box-sizing: border-box;
        text-align: center;
        cursor: pointer;
        position: relative;
        border: none;
      }

      td div {
        width: 32px;
        height: 32px;

        &:hover {
          color: #409eff;
        }
      }

      .disabled_day {
        background-color: #f5f7fa;
        color: #c0c4cc;
        cursor: not-allowed;

        &:hover {
          color: #c0c4cc;
        }
      }

      .select_time_tab {
        border-radius: 50%;
        color: #ffffff;
        background-color: #409eff;
      }

      .currentTime__tb {
        color: #409eff;
      }

      td .date_table_span {
        display: block;
        margin: 0 auto;
        line-height: 32px;
      }
    }
  }
  .vp-option_san {
    position: absolute;
    width: 0;
    height: 0;
    border-width: 9px;
    z-index: 10000;
    border-style: dashed dashed solid;
    border-color: transparent transparent #e4e7ed;
    font-size: 0;
    line-height: 0;
    top: 30px;
    left: 20px;

    &::after {
      content: " ";
      position: absolute;
      width: 0;
      height: 0;
      border-width: 7px;
      z-index: 1;
      border-style: dashed dashed solid;
      border-color: transparent transparent #ffffff;
      font-size: 0;
      line-height: 0;
      top: -5px;
      left: -7px;
    }
  }
}
</style>

Input 组件代码

<template>
  <div class="vp-input">
    <!-- prefix -->
    <div class="vp-input_prefix" v-if="$scopedSlots.prefix">
      <slot name="prefix"></slot>
    </div>
    <input
      v-if="type !== 'textarea'"
      :class="[
        'vp-input-inner',
        hasFocus ? 'vp-input-inner_focus' : '',
        disabled ? 'input-disabled' : '',
        $scopedSlots.prefix ? 'vp-input_inner_prefix' : '',
        $scopedSlots.suffix ? 'vp-input_inner_suffix' : '',
        $scopedSlots.suffix && clearable
          ? 'vp-input_inner_suffix_defaultIcon'
          : '',
      ]"
      :type="isShowPWD ? 'text' : type"
      :placeholder="placeholder"
      :value="value"
      @input="inputHandle"
      :disabled="disabled"
      @blur="blurHandle"
      @focus="focusHandle"
    />
    <!--  -->
    <textarea
      v-else
      class="vp-input-textarea"
      name=""
      id=""
      :cols="cols"
      :rows="rows"
      :value="value"
      @input="textareaInputHandle"
      :readonly="readonly"
      :maxlength="maxlength"
      @blur="blurHandle"
      @focus="focusHandle"
    ></textarea>
    <!-- suffix -->
    <div class="vp-input_suffix" v-if="$scopedSlots.suffix">
      <slot name="suffix"></slot>
    </div>

    <span
      v-if="type === 'password' && value"
      class="iconfont default-icon"
      :class="[
        isShowPWD ? 'icon-eye' : 'icon-eye1',
        $scopedSlots.suffix ? 'default_icon_suffix' : '',
      ]"
      @click="showPWDHandle"
    ></span>
    <span
      v-if="clearable && value"
      class="iconfont icon-clear_circle_outlined default-icon"
      :class="[$scopedSlots.suffix ? 'default_icon_suffix' : '']"
      @click="clearHandle"
    ></span>
  </div>
</template>

<script>
export default {
  name: "vpInput",
  props: {
    type: {
      type: String, // "text" | "password" | "textarea"
      default: "text",
    },
    placeholder: {
      type: String,
      default: "",
    },
    value: {
      type: String | Number,
      default: "",
    },
    disabled: {
      type: Boolean,
      default: false,
    },
    clearable: {
      type: Boolean,
      default: false,
    },
    // textarea 特有
    cols: {
      type: Number,
      default: 20,
    },
    rows: {
      type: Number,
      default: 5,
    },
    readonly: {
      type: Boolean,
      default: false,
    },
    maxlength: {
      type: Number,
      default: 100,
    },
  },
  inject: {
    vpFormItem: {
      default: {},
    },
    vpForm: {
      default: {},
    },
  },
  watch: {
    rule(newRule) {
      newRule.forEach((item) => {
        let trigger = item.trigger;
        if (
          trigger &&
          Object.prototype.toString.call(trigger) === "[object String]"
        ) {
          if (trigger === "input") {
            this.inputRule.push(item);
          } else if (trigger === "blur") {
            this.blurRule.push(item);
          }
        } else if (
          trigger &&
          Object.prototype.toString.call(trigger) === "[object Array]"
        ) {
          trigger.forEach((it) => {
            if (it === "input") {
              this.inputRule.push(item);
            } else if (it === "blur") {
              this.blurRule.push(item);
            }
          });
        }
      });
    },
  },
  data() {
    return {
      isShowPWD: false,
      rule: [],
      inputRule: [],
      blurRule: [],
      ruleMessage: "",
      hasFocus: false,
    };
  },
  watch: {
    value(newVal) {
      this.$emit("input", newVal);
    },
  },
  created() {
    if (this.vpForm.rules && this.vpFormItem.prop) {
      this.rule = this.vpForm.rules[this.vpFormItem.prop];
    }
  },
  mounted() {},
  methods: {
    // 输入input事件
    inputHandle(e) {
      this.$emit("input", e.target.value, e);
      this.$nextTick(() => {
        let inputRule = this.inputRule;
        if (inputRule) {
          inputRule.forEach((rule) => {
            if (rule.required) {
              if (this.value === "") {
                this.ruleMessage = rule.message;
                this.$bus.$emit("ruleChange", {
                  [this.vpFormItem.prop]: {
                    ruleMessage: this.ruleMessage,
                  },
                });
              } else {
                this.ruleMessage = "";
                this.$bus.$emit("ruleChange", {
                  // ruleMessage: this.ruleMessage,
                  [this.vpFormItem.prop]: {
                    ruleMessage: this.ruleMessage,
                  },
                });
              }
            }
          });
        }
      });
    },
    // input blur 事件
    blurHandle(e) {
      this.hasFocus = false;
      this.$emit("blur", e);
      this.$nextTick(() => {
        let blurRule = this.blurRule;
        if (blurRule) {
          blurRule.forEach((rule) => {
            if (rule.required) {
              if (this.value === "") {
                this.ruleMessage = rule.message;
                this.$bus.$emit("ruleChange", {
                  // ruleMessage: this.ruleMessage,
                  [this.vpFormItem.prop]: {
                    ruleMessage: this.ruleMessage,
                  },
                });
              } else {
                if (this.ruleMessage !== "") {
                  this.ruleMessage = "";
                  this.$bus.$emit("ruleChange", {
                    // ruleMessage: this.ruleMessage,
                    [this.vpFormItem.prop]: {
                      ruleMessage: this.ruleMessage,
                    },
                  });
                }
              }
            }
          });
        }
      });
    },
    // focus 事件
    focusHandle(e) {
      this.hasFocus = true;
      this.$emit("focus", e);
    },
    // 切换密码显示
    showPWDHandle() {
      this.isShowPWD = !this.isShowPWD;
    },
    // 清除事件
    clearHandle() {
      this.$emit("input", "");
    },
    // textarea input 事件
    textareaInputHandle(e) {
      this.$emit("input", e.target.value);
    },
  },
};
</script>
<style lang="less" scoped>
// 默认样式
.vp-input {
  width: 100%;
  position: relative;
  display: flex;
  align-items: center;

  .default-icon {
    cursor: pointer;
    position: absolute;
    top: 9px;
    right: 5px;
  }
  .vp-input-inner {
    box-sizing: border-box;
    width: 100%;
    height: 35px;
    border: 1px solid #dcdfe6;
    outline: none;
    color: rgb(148, 146, 144);
    border-radius: 5px;
    border-width: 1px;
    padding: 5px 10px;
    transition: border 0.2s;
    cursor: pointer;

    &:hover {
      border: 1px solid #c0c4cc;
    }
  }

  .vp-input_inner_prefix {
    padding-left: 35px;
  }

  .vp-input_inner_suffix {
    padding-right: 35px;
  }

  .default_icon_suffix {
    right: 40px;
  }

  .vp-input_suffix {
    position: absolute;
    display: inline-block;
    width: 20px;
    padding: 0px 10px;
    right: 0;
  }

  .vp-input_prefix {
    position: absolute;
    display: inline-block;
    width: 20px;
    padding: 0px 10px;
    left: 0;
  }

  .vp-input_inner_suffix_defaultIcon {
    padding-right: 60px;
  }

  .vp-input-textarea {
    outline: none;
    border: 1px solid #dcdfe6;

    &:hover {
      border: 1px solid #c0c4cc;
    }
  }
}

.input-disabled {
  cursor: not-allowed;
  background-color: #f5f7fa;
  border-color: #e4e7ed;
  color: #c0c4cc;
}

.vp-input-inner_focus {
  border: 1px solid #409eff !important;
}
</style>