预约日历组件

<template>
  <div class="calendar">
    <section class="header">
      <van-icon
        name="arrow-left"
        :style="{ color: this.index === 0 ? '#bbbbbb' : '#222222' }"
        @click="
          () => {
            this.isTouching = true
            this.needAnimation = true
            touchEnd('pre')
          }
        "
      />
      {{ selectData.year }}年{{ selectData.month }}月
      <van-icon
        name="arrow"
        :style="{ color: this.index === 1 ? '#bbbbbb' : '#222222' }"
        @click="
          () => {
            this.isTouching = true
            this.needAnimation = true
            touchEnd('next')
          }
        "
      />
    section>
    <ul class="week-area">
      <li class="week-item" v-for="(item, index) in weekArr" :key="index">
        <span class="week-font calendar-item" :style="{ color: ['日', '六'].includes(item) ? '#FF7300' : '#555555' }">
          {{ item }}
        span>
      li>
    ul>
    <section
      ref="calendar"
      class="data-container"
      :style="{
        transitionDuration: `${needHeightAnimation ? transitionDuration : 0}s`
      }"
      @touchstart="touchStart"
      @touchmove="touchMove"
      @touchend="touchEnd"
    >
      <section
        class="month-area"
        :style="{
          transform: `translateX(${-(translateIndex + 1) * 100}%)`,
          transitionDuration: `${needAnimation ? transitionDuration : 0}s`
        }"
      >
        <div
          class="banner-area"
          :style="{
            transform: `translateY(${offsetY}px)`,
            transitionDuration: `${needHeightAnimation ? transitionDuration : 0}s`
          }"
        >
          <ul
            v-for="(monthItem, monthIndex) in allDataArr"
            :key="monthIndex"
            class="data-area"
            :style="{
              transform: `translateX(${(translateIndex + isTouching ? touch.x : 0) * 100}%)`,
              transitionDuration: `${isTouching ? 0 : transitionDuration}s`,
              height: `${itemHeight * lineNum}px`
            }"
          >
            <li
              v-for="(item, index) in monthItem"
              :key="index"
              :class="[
                'data-item',
                { selected: item.days === checkedDay },
                { 'other-item': item.type !== 'normal' && !isWeekView },
                { dayHide: item.dayHide }
              ]"
              :style="`height: ${itemHeight}px`"
              @click.stop="checkoutDate(item)"
            >
              <div class="data-font calendar-item" v-if="item.day">
                {{ item.day }}
                <div class="calendar-item-tip">
                  {{ item.tip }}
                div>
              div>
            li>
          ul>
        div>
      section>
    section>
  div>
template>

<script>
import dayjs from 'dayjs'
import { tools } from '@/utils/tool.js'

export default {
  name: 'Calender',
  props: {
    // 选中的日期
    checkedDay: {
      type: String,
      default: ''
    },
    agoDayHide: {
      type: Number,
      default: dayjs()
        .subtract(1, 'days')
        .valueOf()
    },
    futureDayHide: {},
    useAbleTime: {
      type: Array,
      default: () => [] // 返回的可选时间
    }
  },
  data() {
    return {
      weekArr: ['日', '一', '二', '三', '四', '五', '六'], // 星期数组
      dataArr: [], // 当前可视区域数据
      allDataArr: [], // 轮播数组
      selectData: {}, // 选中日期信息,默认选中当前天 -> year, month, day -> 作用:只是子组件内部的,在日历头部使用的一个值
      isSelectedCurrentDate: false, // 是否点选当前月份信息 (配合月视图减少点击切换时的数组更新)
      translateIndex: 0, // 轮播所在位置
      transitionDuration: 0.3, // 动画持续时间
      needAnimation: true, // 左右滑动是否需要动画
      isTouching: false, // 是否为touch状态
      touchStartPositionX: null, // 初始滑动 X的值
      touchStartPositionY: null, // 初始滑动 Y的值
      touch: {
        // 本次touch事件,横向,纵向滑动的距离
        x: 0,
        y: 0
      },
      isWeekView: false, // 周视图还是月视图
      itemHeight: 52, // 子元素行高
      needHeightAnimation: false, // 高度变化是否需要动画
      offsetY: 0, // 周视图 Y轴偏移量
      lineNum: 0, // 当前视图总行数
      lastWeek: [], // 周视图 前一周数据
      nextWeek: [], // 周视图 后一周数据
      isDelay: true, // 是否延迟 (动画结束在处理数据)
      touchAreaHeight: 40, // 底部区域高度
      touchAreaPadding: 10, // 底部区域padding
      isClicked: false, // 点选元素 (去除周视图切换月份时的动画延迟)
      index: 0 // 可滑动的月份索引 0当前月分 1下一月份
    }
  },
  created() {
    this.checkoutCurrentDate()
  },
  mounted() {
    setTimeout(() => {
      // 查找可预约的日期
      const useableDate1 = this.useAbleTime.find(item => item.usableBookNum > 0 && item.bookDate)?.bookDate
      const useableMonth = dayjs(useableDate1).month() + 1
      if (this.selectData.month !== useableMonth) {
        this.isTouching = true
        this.needAnimation = true
        this.touchEnd('next')
      }
    }, 300)
  },
  watch: {
    dataArr: {
      handler(val) {
        this.changeAllData(val)
      },
      deep: true
    },
    isWeekView(val) {
      if (!val) {
        this.isSelectedCurrentDate = false
        this.changeAllData(this.dataArr)
      }
    }
  },
  methods: {
    // 更新轮播数组
    changeAllData(val) {
      if (this.isSelectedCurrentDate && !this.isWeekView) return
      const preDate = this.getPreMonth()
      console.log('preDate', preDate)
      // 获取上一个月的第一天
      const preDataArr = this.getMonthData(preDate, true)
      const nextDate = this.getNextMonth()
      const nextDataArr = this.getMonthData(nextDate, true)

      if (this.isWeekView) {
        const sliceStart = this.dealWeekViewSliceStart()
        preDataArr.splice(sliceStart, 7, ...this.lastWeek)
        nextDataArr.splice(sliceStart, 7, ...this.nextWeek)
      }

      const delayHandle = isDelay => {
        this.allDataArr = [preDataArr, val, nextDataArr]
        this.needAnimation = false
        this.translateIndex = 0
        if (isDelay) this.isDelay = false
      }

      if (this.isDelay) {
        delayHandle(this.isDelay)
        return
      }

      setTimeout(
        () => {
          delayHandle()
        },
        this.isClicked && this.isWeekView ? 0 : this.transitionDuration * 1000
      )
    },
    // 获取当前日期
    getCurrentDate() {
      const year = new Date().getFullYear()
      const month = new Date().getMonth() + 1
      const day = new Date().getDate()
      this.selectData = {
        year,
        month,
        day,
        days: tools.timeFormat(year, month, day)
      }
    },
    // 获取指定月份数据
    getMonthData(date, unSelected = false) {
      console.log('unSelected', unSelected)
      const { year, month, day } = date
      let dataArr = []
      // 每个月有的天数
      let daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]

      // 判断是否湿润年
      if ((year % 4 === 0 && year % 100 !== 0) || year % 400 === 0) {
        daysInMonth[1] = 29
      }

      // 一个月开始是周几
      const monthStartWeekDay = new Date(year, month - 1, 1).getDay()
      const monthEndWeekDay = new Date(year, month, 1).getDay() || 7

      // 获取上一个月和下一个月的信息
      const preInfo = this.getPreMonth(date)
      const nextInfo = this.getNextMonth()

      // 在当月补充上一个月的几天
      for (let i = 0; i < monthStartWeekDay; i++) {
        let preObj = {
          type: 'pre',
          day: '',
          month: preInfo.month,
          year: preInfo.year
        }
        dataArr.push(preObj)
      }

      for (let i = 0; i < daysInMonth[month - 1]; i++) {
        let days = tools.timeFormat(year, month, i + 1)
        // 获取每一项时间的时间戳
        let t = this.$dayjs(days).valueOf()
        // 获取可用时间
        const useAbleItem = this.useAbleTime?.find(v => v.bookDate === days)

        let dayHide = false
        let tip = ''

        // 未在可用时间段内
        if (!useAbleItem) {
          tip = '未开放'
          dayHide = true
        }
        // 可用预约数为0
        if (useAbleItem?.usableBookNum === 0) {
          tip = '已约满'
        }
        // 当天之前的
        if (this.agoDayHide > t) {
          tip = ''
          dayHide = true
        }

        if (useAbleItem?.usableBookNum > 0) {
          tip = `${useAbleItem?.usableBookNum}`
        }

        let itemObj = {
          type: 'normal',
          day: i + 1,
          month,
          year,
          isSelected: false,
          days,
          dayHide,
          tip
        }
        // console.log('itemObj', itemObj)
        dataArr.push(itemObj)
      }

      // 在当月补充下一个月的几天
      for (let i = 0; i < 7 - monthEndWeekDay; i++) {
        let nextObj = {
          type: 'next',
          day: '',
          month: nextInfo.month,
          year: nextInfo.year
        }
        dataArr.push(nextObj)
      }

      return dataArr
    },
    // 点选切换日期
    checkoutDate(selectData) {
      // 禁止点击的日期
      if (selectData.dayHide || !selectData.day) return

      this.isSelectedCurrentDate = true
      this.isClicked = true

      if (this.isWeekView && selectData.type !== 'normal') {
        this.needAnimation = false
        this.needHeightAnimation = false
      }

      if (selectData.type === 'next') {
        this.translateIndex += 1
        this.dealMonthData('NEXT_MONTH', selectData.day)
        return
      }

      if (selectData.type === 'pre') {
        this.translateIndex -= 1
        this.dealMonthData('PRE_MONTH', selectData.day)
        return
      }

      this.selectData.day = selectData.day

      // 抛出选中的日期

      this.$emit('choseDay', selectData)

      const oldSelectIndex = this.dataArr.findIndex(item => item.isSelected && item.type === 'normal')
      const newSelectIndex = this.dataArr.findIndex(item => item.day === selectData.day && item.type === 'normal')

      if (this.dataArr[oldSelectIndex]) this.$set(this.dataArr[oldSelectIndex], 'isSelected', false)
      if (this.dataArr[newSelectIndex]) this.$set(this.dataArr[newSelectIndex], 'isSelected', true)
    },
    // 获取前(后)一个月的年月日信息
    getPreMonth(date, appointDay = 1) {
      let { year, month } = date || this.selectData
      if (month === 1) {
        year -= 1
        month = 12
      } else {
        month -= 1
      }

      return { year, month, day: appointDay }
    },
    getNextMonth(appointDay = 1) {
      let { year, month } = this.selectData
      if (month === 12) {
        year += 1
        month = 1
      } else {
        month += 1
      }

      return { year, month, day: appointDay }
    },
    // 切换上(下)一月
    handlePreMonth() {
      this.dealMonthData('PRE_MONTH')
    },
    handleNextMonth() {
      this.dealMonthData('NEXT_MONTH')
    },
    // 处理月数据
    dealMonthData(type, appointDay = 1) {
      this.isSelectedCurrentDate = false

      switch (type) {
        case 'PRE_MONTH':
          this.selectData = this.getPreMonth('', appointDay)
          break
        case 'NEXT_MONTH':
          this.selectData = this.getNextMonth(appointDay)
          break
        default:
          break
      }

      // 默认获取当前月的数据
      this.dataArr = this.getMonthData(this.selectData)
      this.lineNum = Math.ceil(this.dataArr.length / 7)
    },
    // 今日
    checkoutCurrentDate() {
      this.isDelay = true
      // 获取当前日期
      this.getCurrentDate()
      this.dealMonthData()
    },
    // touch事件
    touchStart(event) {
      this.isTouching = true
      this.needAnimation = true
      this.isClicked = false

      this.touchStartPositionX = event.touches[0].clientX
      this.touchStartPositionY = event.touches[0].clientY
      this.touch = {
        x: 0
      }
    },
    touchMove(event) {
      const moveX = event.touches[0].clientX - this.touchStartPositionX
      const moveY = event.touches[0].clientY - this.touchStartPositionY

      if (Math.abs(moveX) > Math.abs(moveY)) {
        // 左右
        this.needHeightAnimation = false
        this.touch = {
          x: moveX / this.$refs.calendar.offsetWidth,
          y: 0
        }
      } else {
        // 上下
        this.needHeightAnimation = true
        this.touch = {
          x: 0,
          y: moveY / this.$refs.calendar.offsetHeight
        }
      }
    },
    touchEnd(type) {
      this.isTouching = false
      const { x, y } = this.touch

      // 月视图
      if ((Math.abs(x) > Math.abs(y) && Math.abs(x) > 0.3) || ['pre', 'next'].includes(type)) {
        if (x > 0 || type === 'pre') {
          this.index -= 1
          if (this.index < 0) {
            this.index = 0
          } else {
            // 左
            this.translateIndex -= 1
            console.log('translateIndex', this.translateIndex)
            this.isWeekView ? this.handlePreWeek() : this.handlePreMonth()
          }
        }
        if (x < 0 || type === 'next') {
          // 右
          this.index += 1
          if (this.index > 1) {
            this.index = 1
          } else {
            this.translateIndex += 1
            console.log('translateIndex', this.translateIndex)
            this.isWeekView ? this.handleNextWeek() : this.handleNextMonth()
          }
        }
      }

      this.touch = {
        x: 0,
        y: 0
      }
    },
    // 周视图的位置信息
    getInfoOfWeekView(selectedIndex, length) {
      const indexOfLine = Math.ceil((selectedIndex + 1) / 7)
      const totalLine = Math.ceil(length / 7)
      const sliceStart = (indexOfLine - 1) * 7
      const sliceEnd = sliceStart + 7

      return { indexOfLine, totalLine, sliceStart, sliceEnd }
    },
    // 生成前(后)一周数据
    dealWeekViewSliceStart() {
      const selectedIndex = this.dataArr.findIndex(item => item.isSelected)
      const { indexOfLine, totalLine, sliceStart, sliceEnd } = this.getInfoOfWeekView(
        selectedIndex,
        this.dataArr.length
      )

      this.offsetY = -((indexOfLine - 1) * this.itemHeight)

      // 前一周数据
      if (indexOfLine === 1) {
        const preInfo = this.getPreMonth()
        const preDataArr = this.getMonthData(preInfo, true)
        const preDay = this.dataArr[0].day - 1 || preDataArr[preDataArr.length - 1].day
        const preIndex = preDataArr.findIndex(item => item.day === preDay && item.type === 'normal')
        const { sliceStart: preSliceStart, sliceEnd: preSliceEnd } = this.getInfoOfWeekView(preIndex, preDataArr.length)
        this.lastWeek = preDataArr.slice(preSliceStart, preSliceEnd)
      } else {
        this.lastWeek = this.dataArr.slice(sliceStart - 7, sliceEnd - 7)
      }

      // 后一周数据
      if (indexOfLine >= totalLine) {
        const nextInfo = this.getNextMonth()
        const nextDataArr = this.getMonthData(nextInfo, true)
        const nextDay =
          this.dataArr[this.dataArr.length - 1].type === 'normal' ? 1 : this.dataArr[this.dataArr.length - 1].day + 1
        const nextIndex = nextDataArr.findIndex(item => item.day === nextDay)
        const { sliceStart: nextSliceStart, sliceEnd: nextSliceEnd } = this.getInfoOfWeekView(
          nextIndex,
          nextDataArr.length
        )
        this.nextWeek = nextDataArr.slice(nextSliceStart, nextSliceEnd)
      } else {
        this.nextWeek = this.dataArr.slice(sliceStart + 7, sliceEnd + 7)
      }

      return sliceStart
    },
    // 切换上(下)一周
    handlePreWeek() {
      this.dealWeekData('PRE_WEEK')
    },
    handleNextWeek() {
      this.dealWeekData('NEXT_WEEK')
    },
    // 处理周数据
    dealWeekData(type) {
      const { year, month, day } =
        type === 'PRE_WEEK' ? this.lastWeek.find(item => item.type === 'normal') : this.nextWeek[0]
      this.selectData = { year, month, day }
      this.dataArr = this.getMonthData(this.selectData)
      this.lineNum = Math.ceil(this.dataArr.length / 7)
      this.offsetY -= this.itemHeight
      this.dealWeekViewData()
    },
    // 处理上(下)一周数据
    dealWeekViewData() {
      const sliceStart = this.dealWeekViewSliceStart()
      this.allDataArr[0].splice(sliceStart, 7, ...this.lastWeek)
      this.allDataArr[2].splice(sliceStart, 7, ...this.nextWeek)
    }
  }
}
</script>
<style lang="scss" scoped>
.calendar {
  overflow-x: hidden;
  padding: 12px 10px 14px;
  .header {
    // margin-top: 9px;
    color: #222222;
    text-align: center;
    height: 23px;
    font-size: 16px;
    font-weight: 500;
    line-height: 23px;
    padding: 0 10px;
    @include flexVar(space-between, center);
  }
  .calendar-item {
    display: block;
    min-width: 44px;
    height: auto;
    width: auto;
    text-align: center;
    padding: 4px;
    &-tip {
      font-size: 12px;
      min-height: 16px;
      font-weight: 400;
      transform: scale(0.8);
    }
  }
  .selected .calendar-item {
    color: #fff;
    background: #2689ff;
    box-shadow: 0px 2px 4px 0px rgba(38, 137, 255, 0.3);
    border-radius: 8px;
  }
  .week-area {
    width: 100%;
    display: flex;
    align-items: center;
    justify-content: space-evenly;
  }
  .week-item {
    flex: 0 0 44px;
    width: 44px;
    height: 44px;
    display: flex;
    align-items: center;
    justify-content: center;
    margin-bottom: 8px;
  }
  .week-font {
    font-size: 15px;
    color: #222222;
    font-weight: 500;
  }
  .data-container {
    overflow: hidden;
    position: relative;
  }
  .banner-area {
    width: 300%;
    display: flex;
  }
  .data-area {
    width: 100%;
    height: 100%;
    display: flex;
    flex-flow: row wrap;
  }
  .data-item {
    flex: 0 0 14.285%;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-direction: column;
  }
  .data-font {
    color: #222222;
    font-size: 18px;
    font-weight: 600;
  }
  .other-item .data-font {
    color: #ccc;
  }
  .dayHide .data-font {
    color: #bbbbbb;
  }
  .touch-area {
    width: 100%;
    box-sizing: border-box;
    background-color: #fff;
    position: absolute;
    left: 0;
    bottom: 0;
  }
  .touch-container {
    width: 100%;
    box-sizing: border-box;
    border-top: 0.5px solid #eee;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .touch-item {
    width: 40px;
    height: 5px;
    background: #222222;
    border-radius: 100px;
    opacity: 0.6;
  }
}
</style>
timeFormat(y, m, d) {
    let month = m
    let day = d
    if (m < 10) month = `0${m}`
    if (d < 10) day = `0${day}`
    return `${y}-${month}-${day}`
}

你可能感兴趣的:(javascript,vue)