vue/uni-app之空手撕日历

2,723 阅读3分钟

前言

项目需要,没有合适的轮子,所以,,,

效果(红圈部分):

需求

  • 日期对应星期显示
  • 可选的默认选中日期
  • 今天包括今天之前禁用
  • 某产品的最短预定时间内禁用,如提前三天,则今天明天后天禁用
  • 日期下面显示对应的价格
  • 简易版:如果要当成完全的日历,还需完善

思路

  • 传入年份,月份创建日历;日期价格数组;日期禁用数组

  • 创建一个二维数组,第一维存放一月的周数,第二维存放一周的天数;一维的周数最多有6个数组

  • 每个日期包含年月日三个属性,价格对应的日期也是年月日,禁用的日期页按照年月日,所以匹配一下就可以了

  • 每月天数使用 js 原生判断,即:
    js 中new Date('2019/06/21')传入一个日期,可以用来判断某个月是否有某一天。
    没有的话,返回下一个月一号的构造函数;有的话返回这一天的构造函数;超出31号的则是Invalid。

    // 根据传入的日期构造函数获取年月日
      getYMD(time = new Date()) {
        return {
          _year: time.getFullYear(),
          _month: time.getMonth() + 1,
          _date: time.getDate(),
        }
      },
      // 验证日期是否有效:对应的月份是否有这一天
      validDate(year, month, date) {
        let time = new Date(`${year}/${month}/${date}`);
        const { _year, _month, _date } = this.getYMD(time);
        
        return _year === year && month === _month && date === _date
      },
    

    生成当月天数按周分割的数组

    // 生成一个月的天数
      generateCalendar(year, month) {
        let that = this;
        let weekLen = new Array(7).fill(''); // 一周七天
        let weekday = new Date(`${year}/${month}/01`).getDay(); // 1号星期几
        
        // 重置
        this.monthDay = [[], [], [], [], [], []];
        
        // 生成一月对齐星期的天数,一周以周日开始
        weekLen.map((item, index) => {
          that.monthDay[0].push(
            index < weekday 
            ? '' 
            : (index === weekday) 
              ? {year, month, date: 1}
              : {year, month, date: that.monthDay[0][index-1].date+1}
          );
          that.monthDay[1].push({year, month, date: index + (7 - weekday + 1)});
          that.monthDay[2].push({year, month, date: that.monthDay[1][index].date + 7});
          that.monthDay[3].push({year, month, date: that.monthDay[2][index].date + 7});
          if (
            that.monthDay[3][index].date + 7 <= 31 && 
            that.validDate(year, month, that.monthDay[3][index].date + 7)
          ) {
            that.monthDay[4].push({year, month, date: that.monthDay[3][index].date + 7});
          } else {
            that.monthDay[4].push('');
          }
          if (
            that.monthDay[4][index].date + 7 <= 31 && 
            that.validDate(year, month, that.monthDay[4][index].date + 7)
          ) {
            that.monthDay[5].push({year, month, date: that.monthDay[4][index].date + 7});
          }
        })
      }
    

遇到的问题:

  • 这里在安卓App上面(webview),new Date('2019/06/21')的参数需要用斜杠'/',用短横线new Date('2019-06-21')的话会是 Invalid Date,貌似IOS也是只能使用'/'来获取,兼容性上'/'最靠谱了。。。

代码

父组件使用

<cus-calendar 
    class="calendar" 
    :year="monthActive.year" 
    :month="monthActive.month" 
    :dateActiveDefault="activeDate"
    :saleList="saleList"
    :disabledDate="disabledDate"
    @check-date="checkDate" 
  />

其他:

this.disabledDate = ['2019-06-21','2019-06-22','2019-06-23'];
this.saleList = [
    { date: '2019-06-24', price: 6188 },
    { date: '2019-06-25', price: 6188 },
    { date: '2019-06-26', price: 6188 },
    { date: '2019-07-26', price: 6188 },
    { date: '2019-06-27', price: 6188 },
    { date: '2019-06-28', price: 6188 },
  ];

template

<template>
  <view class="calendar-container">
    <view class="month-bg">
      {{ month }}
    </view>
    <view class="week-title">
      <text class="weekend"></text>
      <text></text>
      <text></text>
      <text></text>
      <text></text>
      <text></text>
      <text class="weekend"></text>
    </view>
    
    <view class="calendar-content">
      <view 
        v-for="(week, weekindex) in monthDay"
        :key="weekindex"
        class="week-month"
      >
        <view
          class="date"
          v-for="(date, dateindex) in week"
          :key="dateindex"
          :class="{'date-disabled': !date || beforeToday(date) || disabledDateFn(date)}"
          @tap="dateTap(date)"
        >
          <view 
            class="date-item"
            :class="{
              'date-active': showPrice(date) && isActiveDate(date),
              'date-active date-active2': !showPrice(date) && isActiveDate(date)
            }"
          >
            <text v-if="isToday(date)">今天</text>
            <text v-else>{{ date.date }}</text>
            <view class="price" v-if="showPrice(date)">¥{{ showPrice(date) }}</view>						
          </view>
        </view>
      </view>
    </view>
  </view>
</template>

js

// <script>
  // 旅游产品 - 日历
  export default {
    name: 'cus-calendar',
    props: {
      theme: {
        type: String,
        default: '#F87D72'
      },
      // 日期下面的价格
      saleList: {
        type: Array,
        default: () => []
      },
      // 禁用的日期
      disabledDate: {
        type: Array,
        default: () => []
      },
      // 默认选中的日期, 如2019-06-24
      dateActiveDefault: {
        type: String,
        default: ''
      },
      year: {
        type: Number,
        default: () => new Date().getFullYear()
      },
      month: {
        type: Number,
        default: () => new Date().getMonth()+1
      }
    },
    data() {
      return {
        newYear: '',
        newMonth: '',
        monthDay: [[], [], [], [], [], []],
        dateActive: {
          year: '',
          month: '',
          date: ''
        }
      }
    },
    mounted() {
      // 默认选中
      if (this.dateActiveDefault) {
        const [year, month, date] = this.dateActiveDefault.split('-');
        this.dateActive = {
          year: +year,
          month: +month,
          date: +date
        }
      }
    },
    watch: {
      month: {
        handler(val) {
          this.newYear = this.year;
          this.newMonth = this.month;
          this.generateCalendar(this.newYear, this.newMonth);
        },
        immediate: true
      }
    },
    methods: {
      // 根据传入的日期构造函数获取年月日
      getYMD(time = new Date()) {
        return {
          _year: time.getFullYear(),
          _month: time.getMonth() + 1,
          _date: time.getDate(),
        }
      },
      // 验证日期是否有效:对应的月份是否有这一天
      validDate(year, month, date) {
        let time = new Date(`${year}/${month}/${date}`);
        const { _year, _month, _date } = this.getYMD(time);
        
        return _year === year && month === _month && date === _date
      },
      // 是否今天
      isToday({ year, month, date }) {
        let time = new Date();
        const { _year, _month, _date } = this.getYMD(time);
        
        return year === _year && month === _month && date === _date
      },
      // 今天之前
      beforeToday({ year, month, date }) {
        let time = new Date();
        const { _year, _month, _date } = this.getYMD(time);
        
        return year <= _year && month <= _month && date <= _date
      },
      // 禁用的日期
      disabledDateFn({ year, month, date }) {
        month = (month+'').padStart(2, '0');
        date = (date+'').padStart(2, '0');
        
        return this.disabledDate.includes(`${year}-${month}-${date}`);
      },
      // 是否选中
      isActiveDate({ year, month, date }) {
        const { year: _year, month: _month, date: _date } = this.dateActive;
        return year === _year && month === _month && date === _date;
      },
      // 点击有效的一天
      dateTap({ year, month, date }) {
        this.dateActive = {
          year,
          month,
          date
        };
        this.$emit('check-date', this.dateActive);
      },
      // 日期下面显示价格
      showPrice({ year, month, date }) {
        if (!year) return;
        
        month = (month+'').padStart(2, '0');
        date = (date+'').padStart(2, '0');
        
        let obj = this.saleList.find(item => item.date === `${year}-${month}-${date}`);
        return obj && obj.price
      },
      // 生成一个月的天数
      generateCalendar(year, month) {
        let that = this;
        let weekLen = new Array(7).fill(''); // 一周七天
        let weekday = new Date(`${year}/${month}/01`).getDay(); // 1号星期几
        
        // 重置
        that.monthDay = [[], [], [], [], [], []];
        
        // 生成一月对齐星期的天数,一周以周日开始
        weekLen.map((item, index) => {
          that.monthDay[0].push(
            index < weekday 
            ? '' 
            : (index === weekday) 
              ? {year, month, date: 1}
              : {year, month, date: that.monthDay[0][index-1].date+1}
          );
          that.monthDay[1].push({year, month, date: index + (7 - weekday + 1)});
          that.monthDay[2].push({year, month, date: that.monthDay[1][index].date + 7});
          that.monthDay[3].push({year, month, date: that.monthDay[2][index].date + 7});
          if (
            that.monthDay[3][index].date + 7 <= 31 && 
            that.validDate(year, month, that.monthDay[3][index].date + 7)
          ) {
            that.monthDay[4].push({year, month, date: that.monthDay[3][index].date + 7});
          } else {
            that.monthDay[4].push('');
          }
          if (
            that.monthDay[4][index].date + 7 <= 31 && 
            that.validDate(year, month, that.monthDay[4][index].date + 7)
          ) {
            that.monthDay[5].push({year, month, date: that.monthDay[4][index].date + 7});
          }
        })
      }
    }
  }
// </script>

样式

/* <style lang="scss" scoped> */
  .calendar-container {
    width: 100%;
    position: relative;
    color: #999;
  }
  .month-bg {
    position: absolute;
    top: 50%;
    left: 50%;
    color: #f6f6f6;
    font-size: 60px;
    transform: translate(-50%, -50%);
    z-index: -1;
  }
  .week-title {
    padding: 20upx 40upx;
    display: flex;
    justify-content: space-between;
    &>text {
      flex: 1;
      text-align: center;
    }
  }
  .weekend {
    color: #F87D72;
  }
  .week-month {
    display: flex;
    justify-content: flex-start;
    padding: 20upx 40upx;
    color: #2b2b2b;
    &>.date {
      flex: 14.285% 0 0;
      text-align: center;
    }
  }
  .date-item {
    width: 60upx;
    height: 60upx;
    position: relative;
    left: 50%;
    margin-left: -30upx;
    line-height: 1;
  }
  .date-disabled {
    color: #999;
    pointer-events: none;
  }
  .price {
    color: #F87D72;
    font-size: 18upx;
  }
  .date-active {
    color: #fff;
    &::after {
      content: '';
      width: 140%;
      height: 140%;
      display: block;
      position: absolute;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      border-radius: 50px;
      background: linear-gradient(right, #F87D72, #F29E97);
      box-shadow: 0 6upx 16upx -6upx #F97C71;
      z-index: -1;
    }
    &>.price {
      color: #fff;
    }
  }
  .date-active2::after {
    transform: translate(-50%, -68%);
  }
/* </style> */