微信小程序自定义"日历组件",使用场景选课程时间段。

129 阅读7分钟

微信小程序自定义"日历组件",使用场景选课程时间段。

GIF 2023-10-29 20-24-44.gif

1、自定义日历组件,命名 s-calendar,组件结构如下:

微信图片_20231029204358.png

index.js
/**
 * @desc 预约日期组件
 */
//处理数组的方法
function mapArray(target) {
  let obj = {};
  let result = [];
  target.map((item) => {
    let key = item.id;
    //如果有这个key,就push一个对象
    if (obj[key]) {
      obj[key].push(item);
      //如果没有这个key,就把对象设置成数组格式(方便后面push进去)
    } else {
      obj[key] = [item];
    }
  });
  //根据obj中有几个对象,挨个push进去,组成最后的数组
  for (const key in obj) {
    const element = obj[key];
    element.forEach((item, index) => {
      if (index == 0) {
        item.clazz = "left";
      } else if (index == element.length - 1) {
        item.clazz = "right";
      } else {
        item.clazz = "center";
      }
      result.push(item);
    });
  }
  return result;
}

let lastDay = "";
Component({
  properties: {
    // (指定日期)预约已满 ["2020-01-15", "2020-01-16"]
    isFullTimes: {
      type: Array,
      value: [],
    },
    // 展示showMonth月数,注意: showMonth等于1时可以切换月份,否则展示showMonth月数
    showMonth: {
      type: Number,
      value: 1,
    },
    // 展示近n天
    showDays: {
      type: Number,
      value: 90,
    },
    // 标题
    title: {
      type: String,
      value: "",
    },
    // 是否可以点击
    disable: {
      type: Boolean,
      value: false
    }
  },
  data: {
    showMonthTemp: "",
    isFullStr: "开课",
    year: "", // 日历头部-年
    month: "", // 日历头部-月
    day: "", // 日历头部-日
    days: [], // 日历主体-当前月的天数
    tomorrowDate: "", // 明天的日期(格式化)
    tomorrowTimestamp: "", // 明天的日期(时间戳)
    selectDate: "", // 选中的日期
    list: [], // [{title:'2022年1月',days:[{day: 1, isFull: "约满", isExpireDate: true, dateTime: "2022-01-01", daysTimestamp: "1640966400"}]}]
    showMonthArr: []
  },
  lifetimes: {
    attached() {
      this.getAfterMonth()
      this.initData();
    },
  },
  /**
   * 监听data中的属性变化
   */
  observers: {
    /* 'isFullTimes': function () {
       this.setIsFullTimes()
     },
     'list': function () {
       this.setIsFullTimes()
     }*/
  },
  methods: {
    initData() {
      let lastDayYMD = this.getDateStr(null, this.data.showDays);
      lastDay = (
        new Date(lastDayYMD.replace(/-/g, "/") + " 00:00:00").getTime() / 1000
      ).toFixed(0);
      let tomorrowDate = this.getDateStr(null, 0);
      let tomorrowTimestamp = (
        new Date(tomorrowDate.replace(/-/g, "/") + " 00:00:00").getTime() / 1000
      ).toFixed(0);
      this.setData({
        tomorrowDate,
        tomorrowTimestamp,
      });

      let now = new Date();

      this.setData({
        year: now.getFullYear(),
        month: now.getMonth() + 1,
        day: now.getDate(),
      });
      this.initDate();
    },
    //设置不选中数据
    setUnSelectDate() {
      this.setData({
        selectDate: "",
      });
    },
    onClosePopup() {
      this.triggerEvent("onClosePopup", {});
    },
    setShowMonth(showMonth) {
      if (showMonth != this.data.showMonth) {
        this.setData({
          showMonth,
          showMonthTemp: showMonth,
          list: [],
        });
        this.initData();
      }
    },
    setIsFullTimes(isFullTimes) {
      isFullTimes = mapArray(isFullTimes); //调用方法,打印的结果如下:
      isFullTimes.forEach((item, index) => {
      });
      this.setData({
        isFullTimes,
      });

      // 重置数据
      if (this.data.showMonthTemp != this.data.showMonth) {
        this.setData({
          selectDate: "",
        });
      }
      for (let i = 0; i < this.data.list.length; i++) {
        let obj = this.data.list[i];
        for (let j = 0; j < obj.days.length; j++) {
          obj.days[j].isFull = "";
        }
      }

      for (let i = 0; i < this.data.isFullTimes.length; i++) {
        for (let j = 0; j < this.data.list.length; j++) {
          let obj = this.data.list[j];
          let isOK = false;
          for (let k = 0; k < obj.days.length; k++) {
            if (isFullTimes[i].time == obj.days[k].dateTime) {
              if (!obj.days[k].bigNinety) {
                obj.days[k].isFull = this.data.isFullStr;
                obj.days[k].id = isFullTimes[i].id;
                obj.days[k].clazz = isFullTimes[i].clazz;
              }
              isOK = true;
              break;
            }
          }
          if (isOK) break;
        }
      }
      this.setData({
        list: this.data.list,
      });
      console.table(this.data.list);
    },
    /**
     * 小于10的补零操作
     * @param m
     * @returns {string}
     */
    add0(m) {
      return m < 10 ? "0" + m : m;
    },
    /**
     * 获取昨天、今天、明天
     * @param today 是需要计算的某一天的日期例如“2018-12-12”,传 null 默认今天
     * @param addDayCount 是要推算的天数, -1是前一天,0是今天,1是后一天
     * @returns {string}
     */
    getDateStr(today, addDayCount) {
      let date;
      if (today) {
        date = new Date(today);
      } else {
        date = new Date();
      }
      date.setDate(date.getDate() + addDayCount); //获取AddDayCount天后的日期
      let y = date.getFullYear();
      let m = this.add0(date.getMonth() + 1); //获取当前月份的日期
      let d = this.add0(date.getDate());
      return y + "-" + m + "-" + d;
    },
    getAfterMonth() {
      let showMonthArr = []
      for (let i = 0; i < this.data.showMonth; i++) {
        let {year, month} = this.afterMonth(i)
        showMonthArr.push({year, month})
      }
      this.setData({
        showMonthArr
      })
      console.log(showMonthArr)
    },
    // n个月前
    afterMonth(n) {
      let date = new Date()
      date.setMonth(date.getMonth() + n)
      date.toLocaleDateString()
      let y = date.getFullYear()
      let m = date.getMonth() + 1
      // m = m < 10 ? '0' + m : m
      return {year: y, month: m}
    },
    /**
     * 添加日历主体-每月的天数
     */
    pushDays() {
      let item = {
        title: this.data.year + "年" + this.data.month + "月",
        days: [],
      };

      //将这个月多少天加入数组days
      let daysTemp = [];
      for (let i = 1; i <= this.getDays(this.data.year, this.data.month); i++) {
        let day = i < 10 ? "0" + i : i;
        // dateTime:每个月对应天的时间格式
        let dateTime =
          this.data.year + "-" + this.add0(this.data.month) + "-" + day;
        // daysTimestamp:每个月对应天的时间戳
        let daysTimestamp = (
          new Date(dateTime.replace(/-/g, "/") + " 00:00:00").getTime() / 1000
        ).toFixed(0);
        let obj = {
          day: i,
          isFull: "",
          isExpireDate: false, // 是否是过期天
          bigNinety: false, // 是否是大于showDays天
          dateTime: dateTime,
          daysTimestamp: daysTimestamp,
          selectDate: false, // 是否选中
        };

        // 小于明天就视为过期 tomorrowTimestamp:明天的时间戳 daysTimestamp:每个月对应的时间戳
        if (
          daysTimestamp < this.data.tomorrowTimestamp ||
          (this.data.showMonth != 1 && daysTimestamp > lastDay)
        ) {
          obj.isExpireDate = true;
          obj.bigNinety = true;
        } else {
          obj.isExpireDate = false;
        }

        daysTemp.push(obj);
        item.days = daysTemp;
      }

      //将下个月要显示的天数加入days
      for (
        let i = 1; i <=
      42 -
      this.getDays(this.year, this.month) -
      this.getWeek(this.year, this.month); i++
      ) {
        let obj = {
          day: i,
          isFull: "",
          isExpireDate: false,
          dateTime: "",
        };
        daysTemp.push(obj);
        item.days = daysTemp;
      }

      //将上个月要显示的天数加入days
      for (let i = 0; i < this.getWeek(this.year, this.month) - 1; i++) {
        let obj = {
          day: "",
          isFull: "",
          isExpireDate: false,
          dateTime: "",
        };
        daysTemp.unshift(obj);
        item.days = daysTemp;
      }
      this.data.list.push(item);
      this.setData({
        list: this.data.list,
      });
    },
    //得到当前年这个月分有多少天
    getDays(Y, M) {
      let day = new Date(Y, M, 0).getDate();
      return day;
    },

    //得到当前年,这个月的一号是周几
    getWeek(Y, M) {
      let oo = this.data.year + "/" + this.data.month + "/" + 1;
      let nn = new Date(oo);
      let week = nn.getDay();
      return week;
    },

    /**
     * 初始化展示的日历
     */
    initDate() {
      this.setData({
        list: []
      })
      for (let i = 0; i < this.data.showMonthArr.length; i++) {
        let obj = this.data.showMonthArr[i]
        this.setData({
          month: obj.month,
          year: obj.year
        })
        this.pushDays();
      }

      this.setIsFullTimes(this.data.isFullTimes)
    },

    onSelectDay(e) {
      debugger
      // if (!this.data.disable) return
      let item = e.currentTarget.dataset.item;
      let hasDay = this.data.isFullTimes.find((d) => d.time == item.dateTime);
      if (item.isExpireDate || hasDay == undefined) {
        return;
      }

      this.data.list[0].days.forEach((d) => (d.selectDate = false));
      this.data.list[0].days.forEach((d) => {
        if (item.id == d.id) {
          d.selectDate = true;
        }
      });

      this.setData({
        list: this.data.list,
      });
      this.triggerEvent("onSelectDay", item);
    },

    onConfirm(){
      this.triggerEvent("onConfirm");
    },

    /**
     * 上个月
     */
    handleShowLastMonth() {
      this.setData({
        list: [],
      });
      if (this.data.month > 1) {
        this.setData({
          month: this.data.month - 1,
        });
      } else if (this.data.year > 1970) {
        this.setData({
          month: 12,
          year: this.data.year - 1,
        });
      } else {
        wx.showToast("不能查找更远的日期");
      }
      this.pushDays();
      this.setIsFullTimes(this.data.isFullTimes);
    },
    /**
     * 下个月
     */
    handleShowNextMonth() {
      this.setData({
        list: [],
      });
      if (this.data.month < 12) {
        this.setData({
          month: this.data.month + 1,
        });
      } else {
        this.setData({
          month: 1,
          year: this.data.year + 1,
        });
      }
      this.pushDays();
      this.setIsFullTimes(this.data.isFullTimes);
    },
  },
});
indes.json
{
  "component": true,
  "usingComponents": {
  }
}
indes.wxml
<view class="calender">
  <view class="date_wrap isIPX">
    <view class="header">
      <view class="tip-top">
        <view class="t1">{{title}}</view>
        <image class="poppu-close"
          src="/img/home/del_msg_icon.png"
          bindtap="onClosePopup"></image>
      </view>
      <view class="line-class"></view>

      <!-- 日历头部 -->
      <view class="week">
        <view class="li"></view>
        <view class="li"></view>
        <view class="li"></view>
        <view class="li"></view>
        <view class="li"></view>
        <view class="li"></view>
        <view class="li"></view>
      </view>
    </view>
    <view class="sc-v ">

      <block wx:for="{{list}}"
        wx:key="title">

        <view class="top-date-box">
          <view wx:if="{{index===0&&showMonth==1}}"
            class="icon_left-p"
            catchtap="handleShowLastMonth">
            <image class="icon_left"
              src="/img/common/calendar_left.png"></image>
          </view>

          <view class="top_date">
            {{item.title}}
          </view>

          <view wx:if="{{index===0&&showMonth==1}}"
            class="icon_left-p"
            catchtap="handleShowNextMonth">
            <image class="icon_left"
              src="/img/common/calendar_right.png"></image>
          </view>
        </view>

        <!-- 日历主体 -->
        <view class="day">
          <view class="li {{daysItem.isExpireDate?'gray':''}} {{daysItem.day&&daysItem.isFull == isFullStr&&daysItem.clazz=='left'?'c_select_bg_left':''}} {{daysItem.day&&daysItem.isFull == isFullStr&&daysItem.clazz=='center'?'c_select_bg_center':''}} {{daysItem.day&&daysItem.isFull == isFullStr&&daysItem.clazz=='right'?'c_select_bg_right':''}} {{daysItem.day&&daysItem.selectDate&&daysItem.clazz=='left'?'c_border_left':''}} {{daysItem.day&&daysItem.selectDate&&daysItem.clazz=='center'?'c_border_center':''}} {{daysItem.day&&daysItem.selectDate&&daysItem.clazz=='right'?'c_border_right':''}}"
            wx:for="{{item.days}}"
            wx:key="id"
            wx:for-item="daysItem"
            catchtap="onSelectDay"
            data-item="{{daysItem}}">

            <view class="day_item">
              <view>
                <view>
                  {{daysItem.day}}
                </view>
                <view class="fs-20">{{daysItem.isFull}}</view>
                <!-- <view class="fs-20" >{{daysItem.isFull}}</view> -->
              </view>
            </view>

          </view>
        </view>
      </block>
    </view>
  </view>
</view>
indes.wxss
.calender {
  background: white;
  border-radius: 16rpx 16rpx 0 0;
}

.calender .top {
  display: flex;
  align-items: center;
}

.calender .icon_left-p {
  padding-top: 15rpx;
  padding-left: 20rpx;
  padding-right: 20rpx;
}

.calender .icon_left {
  vertical-align: top;
  width: 20rpx;
  height: 30rpx;
}

.top-date-box {
  padding: 0 10rpx;
  display: flex;
  align-items: center;
  justify-content: center;
}

.top_date {
  padding-top: 20rpx;
  padding-bottom: 40rpx;
  text-align: center;
  flex: 1;
  font-size: 28rpx;
  font-family: PingFangSC-Medium, PingFang SC;
  font-weight: 500;
  color: #333333;
}

.header {
  width: 100%;
  position: fixed;
  top: 0;
  left: 0;
  z-index: 1;
  background: #ffffff;
  border-radius: 16rpx 16rpx 0 0;
}

.tip-top {
  display: flex;
  align-items: center;
}

.t1 {
  font-size: 32rpx;
  font-weight: 500;
  font-family: PingFangSC-Medium, PingFang SC;
  color: #333333;
  padding-left: 34rpx;
  flex: 1;
}

.poppu-close {
  width: 40rpx;
  height: 40rpx;
  padding: 32rpx;
}

.week {
  padding: 0 10rpx;
  background: #f9f9f9;
  z-index: 1;
  display: flex;
  height: 80rpx;
  line-height: 80rpx;
}

.week .li {
  text-align: center;
  flex: 1;
  width: 14.28%;
  font-size: 28rpx;
  font-family: PingFangSC-Regular, PingFang SC;
  font-weight: 400;
  color: #999999;
}

.day {
  padding: 0 10rpx;
  display: flex;
  flex-direction: row;
  font-size: 28rpx;
  flex-wrap: wrap;
  color: #333333;
}

.day .li {
  width: 14.28%;
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-direction: column;
  color: #333333;
  margin-top: 4rpx;
}

.day_item {
  width: 76rpx;
  height: 80rpx;
  margin-top: 12rpx;
  display: flex;
  text-align: center;
  justify-content: center;
  font-size: 28rpx;
  font-family: PingFangSC-Regular, PingFang SC;
  font-weight: 400;
}

.c_select_bg_center {
  color: #ff561a !important;
  background: #fff7f5;
  border-top: 2rpx solid #fff7f5;
  border-bottom: 2rpx solid #fff7f5;
}

.c_select_bg_left {
  color: #ff561a !important;
  background: #fff7f5;
  border-radius: 16rpx 0 0 16rpx !important;
  border-left: 2rpx solid #fff7f5 !important;
  border-top: 2rpx solid #fff7f5 !important;
  border-bottom: 2rpx solid #fff7f5 !important;
}

.c_select_bg_right {
  color: #ff561a !important;
  background: #fff7f5;
  border-right: 2rpx solid #fff7f5 !important;
  border-top: 2rpx solid #fff7f5 !important;
  border-bottom: 2rpx solid #fff7f5 !important;
  border-radius: 0 16rpx 16rpx 0 !important;
}

.c_border_left {
  border-radius: 16rpx 0 0 16rpx !important;
  border-left: 2rpx solid #ff561a !important;
  border-top: 2rpx solid #ff561a !important;
  border-bottom: 2rpx solid #ff561a !important;
}

.c_border_center {
  border-top: 2rpx solid #ff561a !important;
  border-bottom: 2rpx solid #ff561a !important;
}

.c_border_right {
  border-right: 2rpx solid #ff561a !important;
  border-top: 2rpx solid #ff561a !important;
  border-bottom: 2rpx solid #ff561a !important;
  border-radius: 0 16rpx 16rpx 0 !important;
}

.yqd-icon {
  width: 25rpx;
  height: 25rpx;
}

.gray {
  color: #bebebe !important;
}

.c-bold {
  font-weight: bold;
}

.fs-26 {
  font-size: 26rpx !important;
}

.fs-20 {
  font-size: 20rpx;
}

.c-f {
  color: #ffffff;
}

.sc-v {
  border-radius: 16rpx 16rpx 0 0;
  padding-top: 182rpx;
}

/* 适配iPhoneX系列底部高度 */
.isIPX {
  padding-bottom: constant(safe-area-inset-bottom);
  /*兼容 IOS<11.2*/
  padding-bottom: env(safe-area-inset-bottom);
  /*兼容 IOS>11.2*/
}

.line-class {
  height: 1rpx;
  background-color: #ebebed;
}

.reserve-container {
  display: flex;
  align-items: center;
  padding: 24rpx 0;
}

.img-tip {
  height: 24rpx;
  width: 24rpx;
  margin-top: 8rpx;
}

.reserve-tip-text {
  font-size: 24rpx;
  font-family: PingFangSC-Regular, PingFang SC;
  font-weight: 400;
  color: #faa96d;
}

.reserve-tip-text-bg {
  background: #f9e1e1;
  padding: 16rpx 32rpx;
  margin-bottom: 20rpx;
  font-size: 26rpx;
  font-family: PingFangSC-Regular, PingFang SC;
  font-weight: 400;
  color: #f86363;
  display: flex;
  flex-direction: column;
}

.tip-class {
  display: flex;
  flex-direction: column;
  margin-left: 10rpx;
  flex: 1;
  line-height: 40rpx;
}

2、使用方式:在需要使用的页面引入 s-calendar 组件

微信图片_20231029203455.png

courses-dedail.js
let sCalendar = null;
Page({
  data: {
    // 可以被选择的时间段,id相同视为在一个时间段
    choose_times: [
      {
        "id": 1,
        "time": "2023-11-04"
      },
      {
        "id": 1,
        "time": "2023-11-05"
      },
      {
        "id": 2,
        "time": "2023-11-11"
      },
      {
        "id": 2,
        "time": "2023-11-12"
      },
      {
        "id": 2,
        "time": "2023-11-13"
      }
    ]
  },
 
});

onLoad(options) {
  // 获取日历组件实例
  sCalendar = this.selectComponent("#sCalendar");
  this.onCalendar();
},

// 设置能被选择的课程时间段
onCalendar() {
  sCalendar.setIsFullTimes(this.data.choose_times);
},

// 选中日期回调
onSelectDay(e) {
 console.log(e.detail.id);
}

courses-dedail.json
{
  "navigationBarTitleText": "课程详情",
  "navigationBarBackgroundColor": "#FFFFFF",
  "navigationBarTextStyle": "black",
  "backgroundColor": "#FFFFFF",
  "usingComponents": {
    "s-calendar": "/components/s-calendar/index"
  }
}
courses-dedail.wxml
<s-calendar 
  id="sCalendar"
  title="开课时间预告"
  bindonSelectDay="onSelectDay"
  showMonth="1">
</s-calendar>