基于VUE从零实现一个价格日历(附带讲解,一看就懂)

基于Vue从零实现一个自己的价格日历

  1. 获取当前时间, methods里面定义方法获取当前传入日期的年月份, 绑定time对象中的年月,在这里插入图片描述在这里插入图片描述
    在这里插入图片描述

  2. 实现思路, 每个日期都有一套固定的模板, 共42天如图, 我们只需要拿到当月1号的日期,和拿到当月1号属于一周中的第几天(0-6), 以此推算出当前日历模板42天中每天的日期
    在这里插入图片描述
    代码如图, computed中依赖time中月份或者年份的改变, 计算出每个月的日历模板数据在这里插入图片描述在这里插入图片描述

打印取得的数据结果在这里插入图片描述

  1. 将计算结果遍历至模板上, 模板代码如下
    在这里插入图片描述

  2. 接下来实现日历头部, 样式如下在这里插入图片描述

  3. 我们需要给每个元素绑定点击事件, 点击一次, data中的time对象年或者月会随之递增或者递减,点击后time更新,computed属性会将visibeDays[]重新计算,

在这里插入图片描述
在这里插入图片描述

  1. 现在,一个简单的日历功能已经差不了多少了, 要开始向日历中注入一些自己想要展示的数据, 接收数据
    在这里插入图片描述
根据业务需求,日历上需要展示剩余库存和价格,小伙伴可根据自己实际业务需求自定义数据
data数据: [{ calendar: "2020-11-24", price: "1111.00",stock: 11109}, {calendar: "2020-11-23", price: "1111.00",stock: 1111}]
field数据: [{'stock': '库存'}, {'price': '价格'}] //要展示的字段和key
  1. 将数据渲染至对应日期中
    在这里插入图片描述
    在这里插入图片描述

6.置灰无效日期和过期日期在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

  1. 不属于本月日期,置灰
    在这里插入图片描述
    在这里插入图片描述

<!-- 调用方法!
     data: 传入的所有数据数据 (用户可点击的所有日期) 数组格式
           格式如下
          [{ calendar: "2020-11-24", price: "1111.00",stock: 11109},
           {calendar: "2020-11-23", price: "1111.00",stock: 1111}]

     field: 要在价格日历上展示的字段 如 价格, 库存等... 数组格式
           格式如下
           [{'stock': '库存'}, {'price': '价格'}]
     @formaDate: 用户点击可点击的日期点击回调 回调参数返回用户点击的日期
-->

<template>
  <div v-click-outside style="width: 177px; display: inline-block">
    <el-input type="text" v-model="formaDate" @focus="focus" placeholder="请选择日期"/>
    <!-- <input type="text" v-model="formaDate" @focus="focus" placeholder="请选择日期"/> -->
    <div v-show="isVisble" class="pantal">
      <div class="triangle"></div>
      <div class="header">
        <span @click="prevYear">&lt;</span>
        <span @click="prevMonth">&lt;&lt;</span>
        <span>{{ time.year }}年</span>
        <span>{{ time.month + 1 }}月</span>
        <span @click="nextMonth">&gt;&gt;</span>
        <span @click="nextYear" style="text-align: right">&gt;</span>
      </div>
      <div class="content">
        <div>
          <span v-for="item in weekarr" :key="item">{{ item }}</span>
        </div>
        <div v-for="i in 6" :key="i">
          <span
            v-for="j in 7"
            :key="j"
            class="content-item"
            @click="
              checkDate(
                visibeDays[(i - 1) * 7 + (j - 1)],
                isDisabled(visibeDays[(i - 1) * 7 + (j - 1)])
              )
            "
            :class="[
              { checkGray: !notIsThisMonth(visibeDays[(i - 1) * 7 + (j - 1)]) },
              { active: isActive(visibeDays[(i - 1) * 7 + (j - 1)]) },
              { isdisabeld: isDisabled(visibeDays[(i - 1) * 7 + (j - 1)]) },
              { iscancheck: !isDisabled(visibeDays[(i - 1) * 7 + (j - 1)]) },
            ]"
          >
            <div class="content-info">
              <p style="flex: 1">
                {{ visibeDays[(i - 1) * 7 + (j - 1)].getDate() }}
              </p>

              <p v-for="(ite, index) in field" :key="index">
                {{ showText(visibeDays[(i - 1) * 7 + (j - 1)], ite) }}
              </p>
            </div>
          </span>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: "datePickers",
  directives: {
    // 指令的生命周期
    clickOutside: {
      // 事件绑定事件
      bind(el, bingings, vnode) {
        let hander = (e) => {
          if (el.contains(e.target)) {
            if (!vnode.context.isVisble) {
              //   vnode.context.focus();
            }
          } else {
            if (vnode.context.isVisble) {
              vnode.context.blur();
            }
          }
        };
        el.hander = hander;
        document.addEventListener("click", hander);
      },
      unbind(el) {
        document.removeEventListener("click", el.hander);
      },
    },
  },
  data() {
    let { year, month } = this.getYearMonthDay(this.value || new Date());
    return {
      isVisble: false,
      weekarr: ["日", "一", "二", "三", "四", "五", "六"],
      value: null,
      time: {
        year,
        month,
      },
    };
  },
  methods: {
    focus() {
      this.isVisble = true;
    },
    blur() {
      this.isVisble = false;
    },
    getYearMonthDay(date) {
     
      let year = date.getFullYear();
      let month = date.getMonth();
      let day = date.getDate();
      return { year, month, day };
    },
    getDate(year, month, day) {
      return new Date(year, month, day);
    },
    // 日期模板日期展示不属于这个月
    notIsThisMonth(date) {
      let { year, month } = this.getYearMonthDay(
        this.getDate(this.time.year, this.time.month, 1)
      );
      let { year: y, month: m } = this.getYearMonthDay(date);
      if (year === y && month === m) {
        return true;
      }
    },
    
    isToday(date) {
      let { year, month, day } = this.getYearMonthDay(new Date());
      let { year: y, month: m, day: d } = this.getYearMonthDay(date);
      if (year === y && month === m && day === d) {
        return true;
      }
    },
    // 每个日期要展示的自定义字段
    showText(date, info) {
      let title;
      let k;
      let item;
      for (let key in info) {
        title = info[key];
        k = key;
      }
      let { year: y, month: m, day: d } = this.getYearMonthDay(date);
      this.data.forEach((element) => {
        let splitDateArr = element.calendar.split("-");
        if (
          Number(splitDateArr[0]) === y &&
          Number(splitDateArr[1] - 1) === m &&
          Number(splitDateArr[2]) === d
        ) {
          item = element;
        }
      });
      if (item) {
        let str = title + ":" + item[k];
        return str;
      }
    },
    // 是否禁用 
    isDisabled(date) {
      // let { year, month, day } = utils.getYearMonthDay(new Date())
      let { year: y, month: m, day: d } = this.getYearMonthDay(date);
      let bool = true;
      // 日期小于当前时间 禁用
      if(date.getTime() < new Date().getTime() - (1000 * 60 * 60 * 24)){
        return bool
      }
      
      this.data.forEach((element) => {
        let splitDateArr = element.calendar.split("-");
        if (
          Number(splitDateArr[0]) === y &&
          Number(splitDateArr[1] - 1) === m &&
          Number(splitDateArr[2]) === d
        ) {
          bool = false;
        }
      });
      return bool;
    },
    // 用户选中日期
    checkDate(date, bool) {
      if (bool) return;
      this.time = this.getYearMonthDay(date);
      let { year, month, day } = this.getYearMonthDay(date);

      this.value = date
      this.$emit('formaDate', `${year}-${month + 1}-${day}`)
  
      this.isVisble = false;
    },
    // 点击高亮
    isActive(date) {
       if(!this.value) return 
 
      let { year, month, day } = this.getYearMonthDay(this.value);
      let { year: y, month: m, day: d } = this.getYearMonthDay(date);
      if (year === y && month === m && day === d) {
        return true;
      }
    },
    prevMonth() {
      let d = this.getDate(this.time.year, this.time.month, 1);
      d.setMonth(d.getMonth() - 1);
      this.time = this.getYearMonthDay(d);
    },
    nextMonth() {
      let d = this.getDate(this.time.year, this.time.month, 1);
      d.setMonth(d.getMonth() + 1);
      this.time = this.getYearMonthDay(d);
    },
    prevYear() {
      let d = this.getDate(this.time.year, this.time.month, 1);
      d.setFullYear(d.getFullYear() - 1);
      this.time = this.getYearMonthDay(d);
    },
    nextYear() {
      let d = this.getDate(this.time.year, this.time.month, 1);
      d.setFullYear(d.getFullYear() + 1);
      this.time = this.getYearMonthDay(d);
    },
  },

  // 对象或数组类型必须使用函数
  props: {
    // value: {
    //   type: Date,
    //   default: () => new Date(),
    // },
    disabledDate: {
      type: Array,
      default: () => [],
    },
    data: {
      type: Array,
      default: () => [],
    },
    field: {
      type: Array,
      default: () => [],
    },
  },
  computed: {
    formaDate() {
      if(!this.value) return
      let { year, month, day } = this.getYearMonthDay(this.value);
      return `${year}-${month + 1}-${day}`;
    },
    // 每个月的日期模板
    
    visibeDays() {
      let { year, month } = this.getYearMonthDay(
        this.getDate(this.time.year, this.time.month, 1)
      );
      // 拿到当前年月的一号的时间
      let startDay = this.getDate(year, month, 1);
      // 获取这个月1号是属于这周的第几天 从0-6
      let week = startDay.getDay();
      // 根据1号的时间 和 属于每一周的天数计算出 当月模板 中 第一个日期
      let endDay = startDay - week * 60 * 60 * 24 * 1000;
      let arr = [];
      // 依次计算当页 42天 每天的时间戳
      for (let i = 0; i < 42; i++) {
        arr.push(new Date(endDay + i * 60 * 60 * 24 * 1000));
      }
      return arr;
    },
  },
};
</script>
<style scoped>
div {
  font-size: 16px;
}
.triangle {
  width: 0;
  height: 0;
  overflow: hidden;
  border-top: 10px solid transparent;
  border-left: 10px solid transparent;
  border-right: 10px solid transparent;
  border-bottom: 10px solid #E6E6E6;
  position: absolute;
  top: -20px;
  left: 50px
}
input {
  width: 177px;
  height: 33px;
  border-radius: 3px;
  padding-left: 16px;
  color: #666;
  outline-style: none;
  border: 1px solid #ccc;
}
.pantal {
  position: absolute;
  width: 100%;
  background-color: #ffffff;
  width: 680px;
  box-shadow: 0px 0px 5px #a2a2a2;
  z-index: 1000;
  margin-top: 16px
}
.header {
  /* background: #eee; */
  display: flex;
  justify-content: space-between;
  /* height: 0.3rem; */
}
.header > span {
  float: left;
  width: 95px;
  padding: 0 10px;
  font-size: 18px;
  box-sizing: border-box;
}
.isdisabeld {
  /* background-color: #ccc;
   */
  color: rgb(210, 210, 210);
}
.iscancheck {
  color: #ff335c;
}
.content {
  display: inline-block;
  margin: 0 auto;
  /* border: 1px solid pink; */
  box-sizing: border-box;
}
.content > div {
  text-align: center;
}
.isToday {
  /* color: #999999; */
}
span {
  display: inline-block;
  width: 95px;
  height: 85px;
  line-height: 85px;
  vertical-align: middle;
}
.content-item {
  /* position: relative; */
  height: 85px;
  /* line-height: 20px; */
}
.content-info {
  /* position: absolute;
    top: 50%;
    left: 50%;
    transform: translateX(-50%) translateY(-50%);
    width: 60px;
    height: 60px; */
  display: flex;
  justify-content: space-between;
  flex-direction: column;
}
.content-info p {
  width: 95px;
  height: 20px;
  margin: 0;
  font-size: 8px;
  line-height: 20px;
}
.checkGray {
  color: #666666;
  opacity: 0.4;
}
.active {
  background-color: rgba(24, 144, 255, 0.3);
  color: rgb(24, 144, 255);
  /* background-color:#666; */
  color: #ff335c;
  font-weight: 650;
}
</style>

效果图
小伙伴可以根据自己实际业务需求,进行适当扩展
在这里插入图片描述