手写一个范围选择的日历组件

1,446 阅读5分钟

最近做的一个需求中涉及到一个日历组件,范围选择日期,效果如下:

一开始想找插件来用,后来发现没有相匹配的样式,如果在插件上改动十分麻烦,所以就手动写了一个,主要用到Date对象的api.

首先先介绍一下其中用到的Date api:

let dateObj = new Date() //返回时间对象,不传参数,返回的是当前时间点的时间对象,传递一个事件戳,返回的是传递时间戳的事件对象
dateObj.setDate(1) //将时间对象的日期指定到当前月份的1号
dateObj.setMonth(1) //将时间对象的月份指定到当年的2月份(0-11表示1-12月)
dateObj.setHours(0,0,0,0) //将事件对象指定到0点0分0秒
dateObj.setMonth(12) //当指定到12时,年份自动加1,时间对象会指定到下一年的1月份,组件中就是用这个api来推算之后的月份的.在推算月份之前要调用setDate(1),这样做的目的是因为每个月都有1号,如果不设置setDate,而是取当前的日期,如果当前日期是31号,而下个月份并没有31号,setMonth时就会出现问题.这样每次setMonth,然后调用getTime,获取每个月1号0时0分0秒的事件戳,再通过这个时间戳,获取年,月,日

// 其中还需要计算每个月的天数,方法是先获取月的最后一天的日期,然后就能得知这个月的总天数,方法如下.
let dateObj = new Date()
dateObj.setDate(1) //将日期指定到当前月份的1号,这样做是为了防止当前天是31号,下个月如果没有31号,setMonth会出错
dateObj.setMonth(dateObj.getMonth() + 1) //将月份指定到下一个月的1号
dateObj.setDate(0) //再将日期指定为0,表示是将日期指定为了上一个月的最后一天
dateObj.getDate() //返回最后一天的日期,即为月份的总天数

然后计算出所有的日期,再将日期按着每7天分成一组进行遍历就可以对应出星期.
或者将日期按着星期一-星期日进行分组,进行遍历,下面的代码采用的是第一种方式.

以上就是用到的计算日期的主要api,下面是完整的代码:

<template>
  <div class="calendar">
    <div>
      <div class="header">选择日期</div>
      <ul class="week">
        <li class="item active">日</li>
        <li class="item">一</li>
        <li class="item">二</li>
        <li class="item">三</li>
        <li class="item">四</li>
        <li class="item">五</li>
        <li class="item active">六</li>
      </ul>
    </div>
    <div class="calendar-days" ref="calendarLayout">
      <div ref="calendarCont">
        <div v-for="(calendarItem, index) in calendarList" :key="index">
          <div class="year-month">
            {{ calendarItem.year }}年{{
              calendarItem.month + 1 >= 10
                ? calendarItem.month + 1
                : `0${calendarItem.month + 1}`
            }}月
          </div>
          <div
            class="days-list"
            v-for="(item, index) in calendarItem.daysList"
            :key="index"
          >
            <div
              class="item"
              v-for="(day, index) in item"
              :key="index"
              @click="select(day.time)"
            >
              <div
                :class="[
                  'item-cont',
                  {
                    today: todayTime === day.time,
                    'before-day': day.time < todayTime,
                    'select-start': day.time && day.time === selectTime.start,

                    'select-end': day.time && day.time === selectTime.end,
                    select:
                      day.time &&
                      day.time > selectTime.start &&
                      day.time < selectTime.end
                  }
                ]"
              >
                <div class="left-side"></div>
                <div class="date">{{ day.date }}</div>
                <div class="right-side"></div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>
<script>
export default {
  data() {
    return {
      calendarList: [
        // {
        //   year: 2020,
        //   month: 1,
        //   daysList: [
        //     [{
        //       date: "08",
        //       time: ""
        //     }]
        //   ]
        // }
      ],
      todayTime: "",
      selectTime: {
        start: "",
        end: ""
      }
    };
  },
  methods: {
    select(time) {
      if (time < this.todayTime) {
        return false;
      }
      if (!this.selectTime.start && !this.selectTime.end) {
        this.selectTime.start = time;
      } else if (this.selectTime.start && this.selectTime.end) {
        this.selectTime.start = time;
        this.selectTime.end = "";
      } else if (this.selectTime.start && this.selectTime.start < time) {
        this.selectTime.end = time;
      } else if (this.selectTime.start && this.selectTime.start > time) {
        this.selectTime.start = time;
        this.selectTime.end = "";
      }
    },
    handScroll() {
      this.$refs["calendarLayout"].addEventListener("scroll", () => {
        const bottom = this.$refs["calendarCont"].getBoundingClientRect()
          .bottom;
        const clientH = document.documentElement.clientHeight;
        if (Math.ceil(bottom - clientH) <= 100) {
          let from;
          if (this.calendarList.length) {
            // 获取开始计算的月份
            let monthTime = this.calendarList[this.calendarList.length - 1]
              .daysList[1][0].time;
            let dateObj = new Date(monthTime);
            dateObj.setMonth(new Date(monthTime).getMonth() + 1);
            dateObj.setDate(1);
            from = dateObj.getTime();
          }
          // 再展示一年
          this.calendarList = this.calendarList.concat(
            this.getSomeDate(from, 12)
          );
        }
      });
    },
    getSomeDate(from, howManyMonth = 12) {
      let returnData = [];
      if (!from) {
        let dateObj = new Date();
        // 将时间点设置成每个月的1号
        dateObj.setDate(1);
        from = dateObj.getTime();
      }
      let monthTimeList = [];
      let dataObj = new Date(from);
      // 将时间点设置成每个月的1号
      dataObj.setDate(1);
      monthTimeList.push(dataObj.getTime());
      for (let i = 1; i < howManyMonth; i++) {
        monthTimeList.push(dataObj.setMonth(dataObj.getMonth() + 1));
      }
      monthTimeList.forEach(item => {
        returnData.push({
          year: new Date(item).getFullYear(),
          month: new Date(item).getMonth(),
          daysList: this.getDates(item)
        });
      });
      return returnData;
    },
    getDates(monthTime) {
      let daysArry = [];
      let dateObj = new Date(monthTime);
      let firstDayWeek = dateObj.getDay();
      // 获取这个月的最后一天
      dateObj.setDate(1);
      dateObj.setMonth(dateObj.getMonth() + 1);
      dateObj.setDate(0);
      let lastDayWeek = dateObj.getDay();
      // 获取这个月的最后一天的日期,也就是这个月的总天数
      let days = dateObj.getDate();
      // 获取这个月份每一天的日期和0点的时间戳
      for (let date = 1; date <= days; date++) {
        let dateObj = new Date(monthTime);
        dateObj.setDate(date);
        dateObj.setHours(0, 0, 0, 0);
        daysArry.push({ date: date, time: dateObj.getTime() });
      }
      for (let i = 0; i <= firstDayWeek - 1; i++) {
        daysArry.unshift({ date: "", time: "" });
      }
      for (let i = lastDayWeek + 1; i <= 6; i++) {
        daysArry.push({ date: "", time: "" });
      }
      // 按周分组
      let group = Math.ceil(daysArry.length / 7);
      let daysList = [];
      for (let i = 0; i < group; i++) {
        //分组规律
        // 0  0,7
        // 1  7,14
        // 2  14,20
        // 3  21,27
        // 4  28,34
        daysList.push(daysArry.slice(i * 7, (i + 1) * 7));
      }
      return daysList;
    },
    // 获取今天的日期和时间戳
    getTodayTime() {
      let dateObj = new Date();
      dateObj.setHours(0, 0, 0, 0);
      this.todayTime = dateObj.getTime();
    }
  },
  mounted() {
    this.calendarList = this.getSomeDate();
    this.getTodayTime();
    this.handScroll();
  }
};
</script>
<style lang="scss" scoped>
.calendar {
  display: flex;
  flex-direction: column;
  height: 100vh;
  .calendar-days {
    height: 500px;
    flex: 1 1 auto;
    overflow: auto;
  }
  .header {
    font-size: 36px;
    font-weight: 500;
    text-align: center;
    line-height: 36px;
    padding: 48px 0;
    color: #333;
  }
  .week {
    color: #a6a6a6;
    .active {
      color: #ff3300;
    }
  }
  .days-list {
    height: 104px;
    line-height: 104px;
  }
  .week,
  .days-list {
    display: flex;
    justify-content: space-between;
    .item {
      width: 100px;
      height: 104px;
      flex-grow: 1;
      text-align: center;
      .item-cont {
        font-size: 28px;
        font-weight: 500;
        height: 100%;
        position: relative;
        .left-side,
        .right-side {
          width: 50%;
          height: 100%;
        }
        .left-side {
          float: left;
        }
        .right-side {
          float: right;
        }
        .date {
          position: absolute;
          width: 72px;
          height: 100%;
          top: 0;
          left: 50%;
          margin-left: -36px;
          // background: rgba(255, 51, 0, 1);

          border-radius: 12px;
          // opacity: 0.06;
        }
      }
      .today {
        .date {
          &::before {
            position: absolute;
            content: "今天";
            top: -30px;
            left: 8px;
            // left: 30px;
          }
        }
      }
      .before-day {
        color: #a6a6a6;
      }
      .select-start,
      .select-end {
        .date {
          background: rgba(255, 92, 52, 1);
          color: #fff;
          &::before {
            position: absolute;
            top: -30px;
            left: 8px;
            content: "开始";
          }
        }
      }
      .select-end {
        .date {
          &::before {
            content: "结束";
          }
        }
      }
      .select-start {
        .right-side {
          background: #fff3f1;
        }
      }
      .select-end {
        .left-side {
          background: #fff3f1;
        }
      }
      .select {
        background: #fff3f1;
      }
    }
  }
  .calendar-days {
    .year-month {
      font-weight: 500;
      font-size: 32px;
      color: #333;
      text-align: center;
      padding: 32px 0 16px;
      line-height: 32px;
    }
  }
}
</style>