小程序 - 日历选择

1. 创建component - calendar 

calendar.wxml

<view class="flex box box-tb box-align-center" wx:if="{{calendar}}">
  <view class="calendar pink-color box box-tb">
    <view class="top-handle fs28 box box-lr box-align-center box-pack-center">
      <view class="prev box box-rl" bindtap="choosePrevMonth" data-handle="prev">
        <view class="prev-handle box box-lr box-align-center box-pack-center">《</view>
      </view>
      <view class="date-area box box-lr box-align-center box-pack-center">{{calendar.curYear || "--"}} 年 {{calendar.curMonth || "--"}} 月</view>
      <view class="next box box-lr" bindtap="chooseNextMonth" data-handle="next">
        <view class="next-handle box box-lr box-align-center box-pack-center">》</view>
      </view>
    </view>
    <view class="weeks box box-lr box-align-center">
      <view class="week fs28" wx:for="{{calendar.weeksCh}}" wx:key="{{index}}" data-idx="{{index}}">{{item}}</view>
    </view>
    <view class="perspective">
      <view class="days box box-lr box-wrap {{calendar.leftSwipe ? 'leftRoate' : ''}}  {{calendar.rightSwipe ? 'rightRoate' : ''}}"
        bindtouchstart="calendarTouchstart"
        bindtouchmove="calendarTouchmove"
        bindtouchend="calendarTouchend">
        <view wx:if="{{calendar.empytGrids}}" class="grid disable-day-color box box-align-center box-pack-center"
          wx:for="{{calendar.empytGrids}}"
          wx:key="{{index}}"
          data-idx="{{index}}">
            <view class="day box box-align-center box-pack-center">{{item}}</view>
        </view>
        <view class="grid normal-day-color box box-align-center box-pack-center"
          wx:for="{{calendar.days}}"
          wx:key="{{index}}"
          data-disable="{{item.disable}}"
          data-idx="{{index}}"
          bindtap="tapDayItem">
            <view class="day-with-dot box box-tb box-align-center box-pack-center">
              <view wx:if="{{item.showTodoLabel && calendar.todoLabelPos === 'top'}}" class="{{item.todoText ? 'todo-text' : 'todo-dot'}}" style="background-color: {{calendar.todoLabelColor}}">{{item.todoText}}</view>
              <view class="day border-radius {{item.choosed ? 'day-choosed-color pink-bg' : ''}} {{ item.disable ? 'disable-day-color disable-day-circle' : '' }} box box-align-center box-pack-center">{{item.day}}</view>
              <view wx:if="{{item.showTodoLabel && calendar.todoLabelPos === 'bottom'}}" class="{{item.todoText ? 'todo-text' : 'todo-dot'}}" style="background-color: {{calendar.todoLabelColor}}">{{item.todoText}}</view>
            </view>
        </view>
        <view class="grid disable-day-color box box-align-center box-pack-center"
          wx:for="{{calendar.lastEmptyGrids}}"
          wx:key="{{index}}"
          data-idx="{{index}}">
            <view class="day box box-align-center box-pack-center">{{item}}</view>
        </view>
      </view>
    </view>
  </view>
</view>

calendar.wxss

.box {
  display: flex;
}

.box-lr {
  flex-direction: row;
}

.box-rl {
  flex-direction: row-reverse;
}

.box-tb {
  flex-direction: column;
}

.box-pack-center {
  justify-content: center;
}

.box-align-center {
  align-items: center;
}

.box-wrap {
  flex-wrap: wrap;
}

.flex {
  flex-grow: 1;
}

.bg {
  background-image: linear-gradient(to bottom, #faefe7, #ffcbd7);
  overflow: hidden;
}

.pink-color {
  color: #ff629a;
}

.white-color {
  color: #fff;
}

.fs24 {
  font-size: 24rpx;
}

.fs28 {
  font-size: 28rpx;
}

.fs32 {
  font-size: 32rpx;
}

.fs36 {
  font-size: 36rpx;
}

.calendar {
  width: 100%;
  box-sizing: border-box;
}

.top-handle {
  height: 80rpx;
}

.prev {
  text-align: right;
  height: 80rpx;
}

.next {
  height: 80rpx;
}

.prev-handle {
  width: 80rpx;
  height: 100%;
}

.next-handle {
  width: 80rpx;
  height: 100%;
}

.date-area {
  width: 375rpx;
  height: 80rpx;
  text-align: center;
}

.weeks {
  height: 50rpx;
  line-height: 50rpx;
  opacity: 0.5;
}

.week {
  text-align: center;
  line-height: 104rpx;
}

.grid,
.week {
  width: 104rpx;
  height: 104rpx;
}

.day {
  width: 60rpx;
  height: 60rpx;
  font-size: 26rpx;
  font-weight: 200;
}

.normal-day-color {
  color: #88d2ac;
}

.day-choosed-color {
  color: #fff;
}

.todo-dot {
  width: 10rpx;
  height: 10rpx;
  border-radius: 50%;
  background-color: #cc5226;
}

.todo-text {
  font-size: 22rpx;
  color: #c2c2c2;
}

.day-with-dot {
  height: 72rpx;
}

.disable-day-color {
  color: #cacaca;
}

.disable-day-circle {
  background-color: #f6f6f7;
}

.border-radius {
  border-radius: 50%;
  position: relative;
  left: 0;
  top: 0;
}

.pink-bg {
  background-color: #ff629a;
  transition: all 0.3s;
  animation-name: choosed;
  animation-duration: 0.5s;
  animation-timing-function: linear;
  animation-iteration-count: 1;
}

@keyframes choosed {
  from {
    transform: scale(1);
  }

  50% {
    transform: scale(0.9);
  }

  to {
    transform: scale(1);
  }
}

.purple-bg {
  background-color: #b8b8f1;
}

.right-triangle::after {
  content: '';
  display: block;
  width: 0;
  height: 0;
  border: 15rpx solid transparent;
  border-left-color: #ff629a;
  position: absolute;
  right: -22rpx;
  top: 18rpx;
}

.left-triangle::before {
  content: '';
  display: block;
  width: 0;
  height: 0;
  border: 15rpx solid transparent;
  border-right-color: #ff629a;
  position: absolute;
  left: -22rpx;
  top: 18rpx;
}

.tips {
  text-align: center;
  margin-top: 20rpx;
  margin-bottom: 20rpx;
}

.types {
  background-color: #ffedf4;
  height: 50rpx;
}

.types-desc {
  padding: 0 20rpx;
}

.type-name {
  margin-top: 50rpx;
  margin-bottom: 30rpx;
}

.type-desc {
  padding: 0 35rpx;
  line-height: 38rpx;
}

.explain {
  border-top: 1px solid #eee;
  width: 90%;
  margin: 20rpx 5% 20rpx 5%;
  padding: 20rpx 0;
}

.explain-title {
  font-weight: bold;
  margin-bottom: 15rpx;
}

.explain-item {
  padding: 8rpx 20rpx;
  color: #fff;
}

.left-border-radius {
  border-top-left-radius: 20rpx;
  border-bottom-left-radius: 20rpx;
}

.right-border-radius {
  border-top-right-radius: 20rpx;
  border-bottom-right-radius: 20rpx;
}

.perspective {
  perspective: 750rpx;
}

.leftRoate {
  transition: all 1s;
  transform: rotateY(-5deg);
}

.rightRoate {
  transition: all 1s;
  transform: rotateY(5deg);
}

 

calendar.js

import {
  isLeftSlide,
  isRightSlide,
  getCurrentPage,
  whenChangeMonth,
  renderCalendar,
  whenMulitSelect,
  whenSingleSelect,
  calculateNextWeekDays,
  calculatePrevWeekDays
} from './main.js';

let currentPage = {};

Component({
  options: {
    multipleSlots: true // 在组件定义时的选项中启用多slot支持
  },
  properties: {
    calendar: {
      type: Object
    }
  },
  lifetimes: {
    attached: function() {
      currentPage = getCurrentPage();
    }
  },
  attached: function() {
    currentPage = getCurrentPage();
  },
  methods: {
    /**
     * 选择上一月
     */
    choosePrevMonth() {
      const { curYear, curMonth } = this.data.calendar;
      let newYear = curYear;
      let newMonth = curMonth - 1;
      if (newMonth < 1) {
        newYear = curYear - 1;
        newMonth = 12;
      }
      whenChangeMonth.call(currentPage, {
        curYear,
        curMonth,
        newYear,
        newMonth
      });
      currentPage.setData({
        'calendar.curYear': newYear,
        'calendar.curMonth': newMonth
      });
      renderCalendar.call(currentPage, newYear, newMonth);
    },
    /**
     * 选择下一月
     */
    chooseNextMonth() {
      const { curYear, curMonth } = this.data.calendar;
      let newYear = curYear;
      let newMonth = curMonth + 1;
      if (newMonth > 12) {
        newYear = curYear + 1;
        newMonth = 1;
      }
      whenChangeMonth.call(currentPage, {
        curYear,
        curMonth,
        newYear,
        newMonth
      });
      currentPage.setData({
        'calendar.curYear': newYear,
        'calendar.curMonth': newMonth
      });
      renderCalendar.call(currentPage, newYear, newMonth);
    },
    /**
     * 日期点击事件
     * @param {!object} e 事件对象
     */
    tapDayItem(e) {
      const { idx, disable } = e.currentTarget.dataset;
      if (disable) return;
      let currentSelected = {}; // 当前选中日期
      let { days, selectedDay: selectedDays } = this.data.calendar || []; // 所有选中日期
      const config = currentPage.config;
      const { multi, onTapDay } = config;
      const opts = {
        e,
        idx,
        onTapDay,
        currentSelected,
        selectedDays,
        days: days.slice()
      };
      if (multi) {
        whenMulitSelect.call(currentPage, opts);
      } else {
        whenSingleSelect.call(currentPage, opts);
      }
    },
    /**
     * 日历滑动开始
     * @param {object} e
     */
    calendarTouchstart(e) {
      const t = e.touches[0];
      const startX = t.clientX;
      const startY = t.clientY;
      currentPage.slideLock = true; // 滑动事件加锁
      currentPage.setData({
        'gesture.startX': startX,
        'gesture.startY': startY
      });
    },
    /**
     * 日历滑动中
     * @param {object} e
     */
    calendarTouchmove(e) {
      const self = currentPage;
      if (isLeftSlide.call(self, e)) {
        self.setData({
          'calendar.leftSwipe': 1
        });
        if (currentPage.weekMode)
          return calculateNextWeekDays.call(currentPage);
        this.chooseNextMonth();
      }
      if (isRightSlide.call(self, e)) {
        self.setData({
          'calendar.rightSwipe': 1
        });
        if (currentPage.weekMode)
          return calculatePrevWeekDays.call(currentPage);
        this.choosePrevMonth();
      }
    },
    calendarTouchend(e) {
      currentPage.setData({
        'calendar.leftSwipe': 0,
        'calendar.rightSwipe': 0
      });
    }
  }
});

main.js

let info;
let currentPage = {};

function getSystemInfo() {
  if (info) return info;
  info = wx.getSystemInfoSync();
  return info;
}

function isIos() {
  const sys = getSystemInfo();
  return /iphone|ios/i.test(sys.platform);
}

/**
 * 获取当前页面实例
 */
export function getCurrentPage() {
  const pages = getCurrentPages();
  const last = pages.length - 1;
  return pages[last];
}

/**
 * new Date 区分平台
 * @param {number} year
 * @param {number} month
 * @param {number} day
 */
function newDate(year, month, day) {
  let cur = `${year}-${month}-${day}`;
  if (isIos()) {
    cur = `${year}/${month}/${day}`;
  }
  return new Date(cur);
}

/**
 *  todo 数组去重
 * @param {array} array todo 数组
 */
function uniqueTodoLabels(array = []) {
  let uniqueObject = {};
  let uniqueArray = [];
  array.forEach(item => {
    uniqueObject[`${item.year}-${item.month}-${item.day}`] = item;
  });
  for (let i in uniqueObject) {
    uniqueArray.push(uniqueObject[i]);
  }
  return uniqueArray;
}

/**
 * 上滑
 * @param {object} e 事件对象
 * @returns {boolean} 布尔值
 */
export function isUpSlide(e) {
  const { startX, startY } = this.data.gesture;
  if (this.slideLock) {
    const t = e.touches[0];
    const deltaX = t.clientX - startX;
    const deltaY = t.clientY - startY;
    if (deltaY < -60 && deltaX < 20 && deltaX > -20) {
      this.slideLock = false;
      return true;
    } else {
      return false;
    }
  }
}
/**
 * 下滑
 * @param {object} e 事件对象
 * @returns {boolean} 布尔值
 */
export function isDownSlide(e) {
  const { startX, startY } = this.data.gesture;
  if (this.slideLock) {
    const t = e.touches[0];
    const deltaX = t.clientX - startX;
    const deltaY = t.clientY - startY;
    if (deltaY > 60 && deltaX < 20 && deltaX > -20) {
      this.slideLock = false;
      return true;
    } else {
      return false;
    }
  }
}
/**
 * 左滑
 * @param {object} e 事件对象
 * @returns {boolean} 布尔值
 */
export function isLeftSlide(e) {
  const { startX, startY } = this.data.gesture;
  if (this.slideLock) {
    const t = e.touches[0];
    const deltaX = t.clientX - startX;
    const deltaY = t.clientY - startY;
    if (deltaX < -60 && deltaY < 20 && deltaY > -20) {
      this.slideLock = false;
      return true;
    } else {
      return false;
    }
  }
}
/**
 * 右滑
 * @param {object} e 事件对象
 * @returns {boolean} 布尔值
 */
export function isRightSlide(e) {
  const { startX, startY } = this.data.gesture;
  if (this.slideLock) {
    const t = e.touches[0];
    const deltaX = t.clientX - startX;
    const deltaY = t.clientY - startY;

    if (deltaX > 60 && deltaY < 20 && deltaY > -20) {
      this.slideLock = false;
      return true;
    } else {
      return false;
    }
  }
}

// function info(msg) {
//   console.log('%cInfo: %c' + msg, 'color:#FF0080;font-weight:bold', 'color: #FF509B');
// }

function warn(msg) {
  console.log(
    '%cWarn: %c' + msg,
    'color:#FF6600;font-weight:bold',
    'color: #FF9933'
  );
}

function tips(msg) {
  console.log(
    '%cTips: %c' + msg,
    'color:#00B200;font-weight:bold',
    'color: #00CC33'
  );
}

const conf = {
  /**
   * 计算指定月份共多少天
   * @param {number} year 年份
   * @param {number} month  月份
   */
  getThisMonthDays(year, month) {
    return new Date(year, month, 0).getDate();
  },
  /**
   * 计算指定月份第一天星期几
   * @param {number} year 年份
   * @param {number} month  月份
   */
  getFirstDayOfWeek(year, month) {
    return new Date(Date.UTC(year, month - 1, 1)).getDay();
  },
  /**
   * 计算指定日期星期几
   * @param {number} year 年份
   * @param {number} month  月份
   * @param {number} date 日期
   */
  getDayOfWeek(year, month, date) {
    return new Date(Date.UTC(year, month - 1, date)).getDay();
  },
  /**
   * 渲染日历
   * @param {number} curYear
   * @param {number} curMonth
   * @param {number} curDate
   */
  renderCalendar(curYear, curMonth, curDate) {
    conf.calculateEmptyGrids.call(this, curYear, curMonth);
    conf.calculateDays.call(this, curYear, curMonth, curDate);
    const { todoLabels } = this.data.calendar || {};
    const { afterCalendarRender } = this.config;
    if (todoLabels && todoLabels instanceof Array) {
      conf.setTodoLabels.call(this);
    }

    if (
      afterCalendarRender &&
      typeof afterCalendarRender === 'function' &&
      !this.firstRender
    ) {
      afterCalendarRender(this);
      this.firstRender = true;
    }
  },
  /**
   * 计算当前月份前后两月应占的格子
   * @param {number} year 年份
   * @param {number} month 月份
   */
  calculateEmptyGrids(year, month) {
    conf.calculatePrevMonthGrids.call(this, year, month);
    conf.calculateNextMonthGrids.call(this, year, month);
  },
  /**
   * 计算上月应占的格子
   * @param {number} year 年份
   * @param {number} month 月份
   */
  calculatePrevMonthGrids(year, month) {
    let empytGrids = [];
    const prevMonthDays = conf.getThisMonthDays(year, month - 1);
    const firstDayOfWeek = conf.getFirstDayOfWeek(year, month);
    if (firstDayOfWeek > 0) {
      const len = prevMonthDays - firstDayOfWeek;
      for (let i = prevMonthDays; i > len; i--) {
        empytGrids.push(i);
      }
      this.setData({
        'calendar.empytGrids': empytGrids.reverse()
      });
    } else {
      this.setData({
        'calendar.empytGrids': null
      });
    }
  },
  /**
   * 计算下月应占的格子
   * @param {number} year 年份
   * @param {number} month  月份
   */
  calculateNextMonthGrids(year, month) {
    let lastEmptyGrids = [];
    const thisMonthDays = conf.getThisMonthDays(year, month);
    const lastDayWeek = conf.getDayOfWeek(year, month, thisMonthDays);
    if (+lastDayWeek !== 6) {
      const len = 7 - (lastDayWeek + 1);
      for (let i = 1; i <= len; i++) {
        lastEmptyGrids.push(i);
      }
      this.setData({
        'calendar.lastEmptyGrids': lastEmptyGrids
      });
    } else {
      this.setData({
        'calendar.lastEmptyGrids': null
      });
    }
  },
  /**
   * 设置日历面板数据
   * @param {number} year 年份
   * @param {number} month  月份
   */
  calculateDays(year, month, curDate) {
    let days = [];
    const {
      todayTimestamp,
      disableDays = [],
      enableArea = [],
      enableDays = [],
      enableAreaTimestamp = []
    } = this.data.calendar;
    const thisMonthDays = conf.getThisMonthDays(year, month);
    let expectEnableDaysTimestamp = converEnableDaysToTimestamp(enableDays);
    if (enableArea.length) {
      expectEnableDaysTimestamp = delRepeatedEnableDay(enableDays, enableArea);
    }
    let selectedDay = [];
    if (this.config.noDefault) {
      selectedDay = [];
      this.config.noDefault = false;
    } else {
      selectedDay = curDate
        ? [
            {
              day: curDate,
              choosed: true,
              year,
              month
            }
          ]
        : this.data.calendar.selectedDay;
    }
    for (let i = 1; i <= thisMonthDays; i++) {
      days.push({
        day: i,
        choosed: false,
        year,
        month
      });
    }
    const selectedDayCol = selectedDay.map(
      d => `${d.year}-${d.month}-${d.day}`
    );
    const disableDaysCol = disableDays.map(
      d => `${d.year}-${d.month}-${d.day}`
    );
    days.forEach(item => {
      const cur = `${item.year}-${item.month}-${item.day}`;
      if (selectedDayCol.indexOf(cur) !== -1) item.choosed = true;
      if (disableDaysCol.indexOf(cur) !== -1) item.disable = true;
      const timestamp = newDate(item.year, item.month, item.day).getTime();
      let setDisable = false;
      if (enableAreaTimestamp.length) {
        if (
          (+enableAreaTimestamp[0] > +timestamp ||
            +timestamp > +enableAreaTimestamp[1]) &&
          !expectEnableDaysTimestamp.includes(+timestamp)
        ) {
          setDisable = true;
        }
      } else if (
        expectEnableDaysTimestamp.length &&
        !expectEnableDaysTimestamp.includes(+timestamp)
      ) {
        setDisable = true;
      }
      if (setDisable) {
        item.disable = true;
        item.choosed = false;
      }
      if (
        this.config.disablePastDay &&
        timestamp - todayTimestamp < 0 &&
        !item.disable
      ) {
        item.disable = true;
      }
    });
    const tmp = { 'calendar.days': days };
    if (curDate) {
      tmp['calendar.selectedDay'] = selectedDay;
    }
    this.setData(tmp);
  },
  /**
   * 当改变月份时触发
   * @param {object} param
   */
  whenChangeMonth({ curYear, curMonth, newYear, newMonth }) {
    const { whenChangeMonth } = this.config || {};
    if (typeof whenChangeMonth === 'function') {
      whenChangeMonth(
        {
          year: curYear,
          month: curMonth
        },
        {
          year: newYear,
          month: newMonth
        }
      );
    }
  },
  /**
   * 点击日期后触发事件
   * @param {object} currentSelected 当前选择的日期
   * @param {array} selectedDays  多选状态下选中的日期
   */
  afterTapDay(currentSelected, selectedDays) {
    const config = this.config;
    const { multi, afterTapDay } = config;
    if (afterTapDay && typeof afterTapDay === 'function') {
      if (!multi) {
        config.afterTapDay(currentSelected);
      } else {
        config.afterTapDay(currentSelected, selectedDays);
      }
    }
  },
  /**
   * 多选
   * @param {object} opts
   */
  whenMulitSelect(opts = {}) {
    let { currentSelected, selectedDays } = opts;
    const { days, idx, onTapDay, e } = opts;
    days[idx].choosed = !days[idx].choosed;
    if (!days[idx].choosed) {
      days[idx].cancel = true; // 该次点击是否为取消日期操作
      currentSelected = days[idx];
      selectedDays = selectedDays.filter(
        item =>
          `${item.year}-${item.month}-${item.day}` !==
          `${currentSelected.year}-${currentSelected.month}-${
            currentSelected.day
          }`
      );
    } else {
      currentSelected = days[idx];
      selectedDays.push(currentSelected);
    }
    if (onTapDay && typeof onTapDay === 'function') {
      return this.config.onTapDay(currentSelected, e);
    }
    this.setData({
      'calendar.days': days,
      'calendar.selectedDay': selectedDays
    });
    conf.afterTapDay.call(this, currentSelected, selectedDays);
  },
  /**
   * 单选
   * @param {object} opts
   */
  whenSingleSelect(opts = {}) {
    let { currentSelected, selectedDays = [] } = opts;
    let shouldMarkerTodoDay = [];
    const { days, idx, onTapDay, e } = opts;
    const { month: sMonth, year: sYear } = selectedDays[0] || {};
    const { month: dMonth, year: dYear } = days[0] || {};
    const { calendar = {} } = this.data;
    if (sMonth === dMonth && sYear === dYear && !this.weekMode) {
      days[selectedDays[0].day - 1].choosed = false;
    }
    if (this.weekMode) {
      days.forEach((item, idx) => {
        if (item.day === selectedDays[0].day) days[idx].choosed = false;
      });
    }
    if (calendar.todoLabels) {
      // 过滤所有待办日期中当月有待办事项的日期
      shouldMarkerTodoDay = calendar.todoLabels.filter(
        item => +item.year === dYear && +item.month === dMonth
      );
    }
    shouldMarkerTodoDay.forEach(item => {
      // hasTodo 是否有待办事项
      if (this.weekMode) {
        days.forEach((_item, idx) => {
          if (+_item.day === +item.day) {
            const day = days[idx];
            day.hasTodo = true;
            day.todoText = item.todoText;
            if (
              selectedDays &&
              selectedDays.length &&
              +selectedDays[0].day === +item.day
            ) {
              day.showTodoLabel = true;
            }
          }
        });
      } else {
        const day = days[item.day - 1];
        day.hasTodo = true;
        day.todoText = item.todoText;
        if (
          selectedDays &&
          selectedDays.length &&
          +selectedDays[0].day === +item.day
        ) {
          // showTodoLabel 是否显示待办标记
          days[selectedDays[0].day - 1].showTodoLabel = true;
        }
      }
    });
    const currentDay = days[idx];
    if (currentDay.showTodoLabel) currentDay.showTodoLabel = false;
    currentDay.choosed = true;
    currentSelected = currentDay;
    if (onTapDay && typeof onTapDay === 'function') {
      return this.config.onTapDay(currentSelected, e);
    }
    this.setData({
      'calendar.days': days,
      'calendar.selectedDay': [currentSelected]
    });
    conf.afterTapDay.call(this, currentSelected);
  },
  /**
   * 设置代办事项标志
   * @param {object} options 代办事项配置
   */
  setTodoLabels(options = {}) {
    const { calendar } = this.data;
    if (!calendar || !calendar.days) {
      return warn('请等待日历初始化完成后再调用该方法');
    }
    const days = calendar.days.slice();
    const { curYear, curMonth } = calendar;
    const { days: todoDays = [], pos = 'bottom', dotColor = '' } = options;
    const { todoLabels = [], todoLabelPos, todoLabelColor } = calendar;
    const shouldMarkerTodoDay = todoDays.filter(
      item => +item.year === curYear && +item.month === curMonth
    );
    let currentMonthTodoLabels = todoLabels.filter(
      item => +item.year === curYear && +item.month === curMonth
    );
    shouldMarkerTodoDay.concat(currentMonthTodoLabels).forEach(item => {
      let target = {};
      if (this.weekMode) {
        target = days.find(d => +d.day === +item.day);
      } else {
        target = days[item.day - 1];
      }
      if (target) target.showTodoLabel = !target.choosed;
      if (target.showTodoLabel && item.todoText)
        target.todoText = item.todoText;
    });
    const o = {
      'calendar.days': days,
      'calendar.todoLabels': uniqueTodoLabels(todoDays.concat(todoLabels))
    };
    if (pos && pos !== todoLabelPos) o['calendar.todoLabelPos'] = pos;
    if (dotColor && dotColor !== todoLabelColor) {
      o['calendar.todoLabelColor'] = dotColor;
    }
    this.setData(o);
  },
  /**
   * 筛选待办事项
   * @param {array} todos 需要删除待办标记的日期
   */
  filterTodos(todos) {
    const { todoLabels } = this.data.calendar;
    const deleteTodo = todos.map(
      item => `${item.year}-${item.month}-${item.day}`
    );
    return todoLabels.filter(
      item =>
        deleteTodo.indexOf(`${item.year}-${item.month}-${item.day}`) === -1
    );
  },
  /**
   *  删除指定日期的待办标识
   * @param {array} todos 需要删除待办标记的日期
   */
  deleteTodoLabels(todos) {
    if (!(todos instanceof Array) || !todos.length) return;
    const todoLabels = conf.filterTodos.call(this, todos);
    const { days, curYear, curMonth } = this.data.calendar;
    const currentMonthTodoLabels = todoLabels.filter(
      item => curYear === +item.year && curMonth === +item.month
    );
    days.forEach(item => {
      item.showTodoLabel = false;
    });
    currentMonthTodoLabels.forEach(item => {
      days[item.day - 1].showTodoLabel = !days[item.day - 1].choosed;
    });
    this.setData({
      'calendar.days': days,
      'calendar.todoLabels': todoLabels
    });
  },
  /**
   * 清空所有日期的待办标识
   */
  clearTodoLabels() {
    const { days = [] } = this.data.calendar;
    const _days = [].concat(days);
    _days.forEach(item => {
      item.showTodoLabel = false;
    });
    this.setData({
      'calendar.days': _days,
      'calendar.todoLabels': []
    });
  },
  /**
   * 跳转至今天
   */
  jumpToToday() {
    const date = new Date();
    const curYear = date.getFullYear();
    const curMonth = date.getMonth() + 1;
    const curDate = date.getDate();
    const timestamp = newDate(curYear, curMonth, curDate).getTime();
    this.setData({
      'calendar.curYear': curYear,
      'calendar.curMonth': curMonth,
      'calendar.selectedDay': [
        {
          year: curYear,
          day: curDate,
          month: curMonth,
          choosed: true
        }
      ],
      'calendar.todayTimestamp': timestamp
    });
    conf.renderCalendar.call(this, curYear, curMonth, curDate);
  },
  /**
   * 更新当前年月
   */
  updateCurrYearAndMonth(type) {
    let { days, curYear, curMonth } = this.data.calendar;
    const { month: firstMonth } = days[0];
    const { month: lastMonth } = days[days.length - 1];
    const lastDayOfThisMonth = conf.getThisMonthDays(curYear, curMonth);
    const lastDayOfThisWeek = days[days.length - 1];
    const firstDayOfThisWeek = days[0];
    if (
      (lastDayOfThisWeek.day + 7 > lastDayOfThisMonth ||
        (curMonth === firstMonth && firstMonth !== lastMonth)) &&
      type === 'next'
    ) {
      curMonth = curMonth + 1;
      if (curMonth > 12) {
        curYear = curYear + 1;
        curMonth = 1;
      }
    } else if (
      (+firstDayOfThisWeek.day <= 7 ||
        (curMonth === lastMonth && firstMonth !== lastMonth)) &&
      type === 'prev'
    ) {
      curMonth = curMonth - 1;
      if (curMonth <= 0) {
        curYear = curYear - 1;
        curMonth = 12;
      }
    }
    return {
      Uyear: curYear,
      Umonth: curMonth
    };
  },
  /**
   * 计算周视图下当前这一周和当月的最后一天
   */
  calculateLastDay() {
    const { days, curYear, curMonth } = this.data.calendar;
    const lastDayInThisWeek = days[days.length - 1].day;
    const lastDayInThisMonth = conf.getThisMonthDays(curYear, curMonth);
    return { lastDayInThisWeek, lastDayInThisMonth };
  },
  /**
   * 计算周视图下当前这一周第一天
   */
  calculateFirstDay() {
    const { days } = this.data.calendar;
    const firstDayInThisWeek = days[0].day;
    return { firstDayInThisWeek };
  },
  /**
   * 当月第一周所有日期范围
   * @param {number} year
   * @param {number} month
   */
  firstWeekInMonth(year, month) {
    const firstDay = conf.getDayOfWeek(year, month, 1);
    const firstWeekDays = [1, 1 + (6 - firstDay)];
    const { days } = this.data.calendar;
    const daysCut = days.slice(firstWeekDays[0] - 1, firstWeekDays[1]);
    return daysCut;
  },
  /**
   * 当月最后一周所有日期范围
   * @param {number} year
   * @param {number} month
   */
  lastWeekInMonth(year, month) {
    const lastDay = conf.getThisMonthDays(year, month);
    const lastDayWeek = conf.getDayOfWeek(year, month, lastDay);
    const lastWeekDays = [lastDay - lastDayWeek, lastDay];
    const { days } = this.data.calendar;
    const daysCut = days.slice(lastWeekDays[0] - 1, lastWeekDays[1]);
    return daysCut;
  },
  /**
   * 渲染日期之前初始化已选日期
   * @param {array} days 当前日期数组
   */
  initSelectedDay(days) {
    const daysCopy = days.slice();
    const { selectedDay = [], todoLabels = [] } = this.data.calendar;
    const selectedDayStr = selectedDay.map(
      item => `${item.year}+${item.month}+${item.day}`
    );
    const todoLabelsCol = todoLabels.map(d => `${d.year}-${d.month}-${d.day}`);
    daysCopy.forEach(item => {
      if (
        selectedDayStr.indexOf(`${item.year}+${item.month}+${item.day}`) !== -1
      ) {
        item.choosed = true;
      } else {
        item.choosed = false;
      }
      const idx = todoLabelsCol.indexOf(
        `${item.year}-${item.month}-${item.day}`
      );
      if (idx !== -1) {
        item.showTodoLabel = !item.choosed;
        if (item.showTodoLabel && todoLabels[idx].todoText)
          item.todoText = todoLabels[idx].todoText;
      }
    });
    return daysCopy;
  },
  /**
   * 周视图下设置可选日期范围
   * @param {object} days 当前展示的日期
   */
  setEnableAreaOnWeekMode(days) {
    let {
      todayTimestamp,
      enableAreaTimestamp = [],
      enableDaysTimestamp = []
    } = this.data.calendar;
    days.forEach(item => {
      const timestamp = newDate(item.year, item.month, item.day).getTime();

      let setDisable = false;
      if (enableAreaTimestamp.length) {
        if (
          (+enableAreaTimestamp[0] > +timestamp ||
            +timestamp > +enableAreaTimestamp[1]) &&
          !enableDaysTimestamp.includes(+timestamp)
        ) {
          setDisable = true;
        }
      } else if (
        enableDaysTimestamp.length &&
        !enableDaysTimestamp.includes(+timestamp)
      ) {
        setDisable = true;
      }
      if (setDisable) {
        item.disable = true;
        item.choosed = false;
      }
      if (
        this.config.disablePastDay &&
        timestamp - todayTimestamp < 0 &&
        !item.disable
      ) {
        item.disable = true;
      }
    });
  },
  /**
   * 计算下一周的日期
   */
  calculateNextWeekDays() {
    let { lastDayInThisWeek, lastDayInThisMonth } = conf.calculateLastDay.call(
      this
    );
    let { curYear, curMonth } = this.data.calendar;
    let days = [];
    if (lastDayInThisMonth - lastDayInThisWeek >= 7) {
      const { Uyear, Umonth } = conf.updateCurrYearAndMonth.call(this, 'next');
      curYear = Uyear;
      curMonth = Umonth;
      for (let i = lastDayInThisWeek + 1; i <= lastDayInThisWeek + 7; i++) {
        days.push({
          year: curYear,
          month: curMonth,
          day: i
        });
      }
    } else {
      for (let i = lastDayInThisWeek + 1; i <= lastDayInThisMonth; i++) {
        days.push({
          year: curYear,
          month: curMonth,
          day: i
        });
      }
      const { Uyear, Umonth } = conf.updateCurrYearAndMonth.call(this, 'next');
      curYear = Uyear;
      curMonth = Umonth;
      for (let i = 1; i <= 7 - (lastDayInThisMonth - lastDayInThisWeek); i++) {
        days.push({
          year: curYear,
          month: curMonth,
          day: i
        });
      }
    }
    days = conf.initSelectedDay.call(this, days);
    conf.setEnableAreaOnWeekMode.call(this, days);
    this.setData({
      'calendar.curYear': curYear,
      'calendar.curMonth': curMonth,
      'calendar.days': days
    });
  },
  /**
   * 计算上一周的日期
   */
  calculatePrevWeekDays() {
    let { firstDayInThisWeek } = conf.calculateFirstDay.call(this);
    let { curYear, curMonth } = this.data.calendar;
    let days = [];

    if (firstDayInThisWeek - 7 > 0) {
      const { Uyear, Umonth } = conf.updateCurrYearAndMonth.call(this, 'prev');
      curYear = Uyear;
      curMonth = Umonth;
      for (let i = firstDayInThisWeek - 7; i < firstDayInThisWeek; i++) {
        days.push({
          year: curYear,
          month: curMonth,
          day: i
        });
      }
    } else {
      let temp = [];
      for (let i = 1; i < firstDayInThisWeek; i++) {
        temp.push({
          year: curYear,
          month: curMonth,
          day: i
        });
      }
      const { Uyear, Umonth } = conf.updateCurrYearAndMonth.call(this, 'prev');
      curYear = Uyear;
      curMonth = Umonth;
      const prevMonthDays = conf.getThisMonthDays(curYear, curMonth);
      for (
        let i = prevMonthDays - Math.abs(firstDayInThisWeek - 7);
        i <= prevMonthDays;
        i++
      ) {
        days.push({
          year: curYear,
          month: curMonth,
          day: i
        });
      }
      days = days.concat(temp);
    }
    days = conf.initSelectedDay.call(this, days);
    conf.setEnableAreaOnWeekMode.call(this, days);
    this.setData({
      'calendar.curYear': curYear,
      'calendar.curMonth': curMonth,
      'calendar.days': days
    });
  },
  /**
   * 计算当前选中日期所在周,并重新渲染日历
   * @param {object} currentDay 当前选择日期
   */
  selectedDayWeekAllDays(currentDay) {
    let { days, curYear, curMonth } = this.data.calendar;
    let { year, month, day } = currentDay;
    let lastWeekDays = conf.lastWeekInMonth.call(this, year, month);
    let empytGrids = [];
    let lastEmptyGrids = [];
    const firstWeekDays = conf.firstWeekInMonth.call(this, year, month);
    // 判断选中日期的月份是否与当前月份一致
    if (curYear !== year || curMonth !== month) day = 1;
    if (curYear !== year) year = curYear;
    if (curMonth !== month) month = curMonth;
    if (firstWeekDays.find(item => item.day === day)) {
      // 当前选择的日期为该月第一周
      let temp = [];
      const lastDayInThisMonth = conf.getThisMonthDays(year, month - 1);
      const { Uyear, Umonth } = conf.updateCurrYearAndMonth.call(this, 'prev');
      curYear = Uyear;
      curMonth = Umonth;
      for (
        let i = lastDayInThisMonth - (7 - firstWeekDays.length) + 1;
        i <= lastDayInThisMonth;
        i++
      ) {
        temp.push({
          year: curYear,
          month: curMonth,
          day: i
        });
      }
      days = temp.concat(firstWeekDays);
    } else if (lastWeekDays.find(item => item.day === day)) {
      // 当前选择的日期为该月最后一周
      const temp = [];
      if (lastWeekDays && lastWeekDays.length < 7) {
        const { Uyear, Umonth } = conf.updateCurrYearAndMonth.call(
          this,
          'next'
        );
        curYear = Uyear;
        curMonth = Umonth;
        for (let i = 1, len = 7 - lastWeekDays.length; i <= len; i++) {
          temp.push({
            year: curYear,
            month: curMonth,
            day: i
          });
        }
      }
      days = lastWeekDays.concat(temp);
    } else {
      const week = conf.getDayOfWeek(year, month, day);
      const range = [day - week, day + (6 - week)];
      days = days.slice(range[0] - 1, range[1]);
    }
    days = conf.initSelectedDay.call(this, days);
    this.setData({
      'calendar.days': days,
      'calendar.empytGrids': empytGrids,
      'calendar.lastEmptyGrids': lastEmptyGrids
    });
  },
  /**
   * 周、月视图切换
   * @param {string} view  视图 [week, month]
   */
  switchWeek(view) {
    if (this.config.multi) return warn('多选模式不能切换周月视图');
    const { selectedDay = [], curYear, curMonth } = this.data.calendar;
    if (!selectedDay.length) return;
    const currentDay = selectedDay[0];
    if (view === 'week') {
      if (this.weekMode) return;
      this.weekMode = true;
      conf.selectedDayWeekAllDays.call(this, currentDay);
    } else {
      this.weekMode = false;
      let { year, month, day } = currentDay;
      if (curYear !== year || curMonth !== month) day = 1;
      conf.renderCalendar.call(this, curYear, curMonth, day);
    }
  },
  /**
   * 禁用指定日期
   * @param {array} days  禁用
   */
  disableDays(data) {
    const { disableDays = [], days } = this.data.calendar;
    if (Object.prototype.toString.call(data) !== '[object Array]') {
      return warn('disableDays 参数为数组');
    }
    const _disableDays = data.concat(disableDays);
    const disableDaysCol = _disableDays.map(
      d => `${d.year}-${d.month}-${d.day}`
    );
    days.forEach(item => {
      const cur = `${item.year}-${item.month}-${item.day}`;
      if (disableDaysCol.indexOf(cur) !== -1) item.disable = true;
    });
    this.setData({
      'calendar.days': days,
      'calendar.disableDays': _disableDays
    });
  }
};

export const whenChangeMonth = conf.whenChangeMonth;
export const renderCalendar = conf.renderCalendar;
export const whenSingleSelect = conf.whenSingleSelect;
export const whenMulitSelect = conf.whenMulitSelect;
export const calculatePrevWeekDays = conf.calculatePrevWeekDays;
export const calculateNextWeekDays = conf.calculateNextWeekDays;

/**
 * 获取已选择的日期
 */
export const getSelectedDay = () => {
  return currentPage && currentPage.data.calendar.selectedDay;
};
/**
 * 跳转至指定日期
 */
export const jump = (year, month, day) => {
  const self = currentPage;
  const { selectedDay } = self.data.calendar;
  if (
    selectedDay && selectedDay[0]
    +selectedDay[0].year === +year &&
    +selectedDay[0].month === +month &&
    +selectedDay[0].day === +day
  ) {
    return;
  }
  if (year && month) {
    if (typeof +year !== 'number' || typeof +month !== 'number') {
      return warn('jump 函数年月日参数必须为数字');
    }
    let tmp = {
      'calendar.curYear': year,
      'calendar.curMonth': month
    };
    self.setData(tmp, () => {
      if (typeof +day === 'number') {
        return conf.renderCalendar.call(self, year, month, day);
      }
      conf.renderCalendar.call(self, year, month);
    });
    return;
  }
  conf.jumpToToday.call(self);
};
/**
 * 设置代办事项日期标记
 * @param {object} todos  待办事项配置
 * @param {string} [todos.pos] 标记显示位置,默认值'bottom' ['bottom', 'top']
 * @param {string} [todos.dotColor] 标记点颜色,backgroundColor 支持的值都行
 * @param {object[]} todos.days 需要标记的所有日期,如:[{year: 2015, month: 5, day: 12}],其中年月日字段必填
 */
export const setTodoLabels = todos => {
  conf.setTodoLabels.call(currentPage, todos);
};
/**
 * 删除指定日期待办标记
 * @param {array} todos 需要删除的待办日期数组
 */
export const deleteTodoLabels = todos => {
  conf.deleteTodoLabels.call(currentPage, todos);
};
/**
 * 清空所有待办标记
 */
export const clearTodoLabels = () => {
  conf.clearTodoLabels.call(currentPage);
};
/**
 * 切换周月视图
 * @param {string} view 视图模式[week, month]
 */
export const switchView = view => {
  conf.switchWeek.call(currentPage, view);
};
/**
 * 禁用指定日期
 * @param {array} days 日期
 * @param {number} [days.year]
 * @param {number} [days.month]
 * @param {number} [days.day]
 */
export const disableDay = (days = []) => {
  conf.disableDays.call(currentPage, days);
};

/**
 * 指定可选日期及可选日期数组去重
 * @param {array} enableDays 特定可选日期数组
 * @param {array} enableArea 可选日期区域数组
 */
function delRepeatedEnableDay(enableDays = [], enableArea = []) {
  let _startTimestamp;
  let _endTimestamp;
  if (enableArea.length === 2) {
    const { startTimestamp, endTimestamp } = convertEnableAreaToTimestamp(
      enableArea
    );
    _startTimestamp = startTimestamp;
    _endTimestamp = endTimestamp;
  }
  const enableDaysTimestamp = converEnableDaysToTimestamp(enableDays);
  const tmp = enableDaysTimestamp.filter(
    item => item < _startTimestamp || item > _endTimestamp
  );
  return tmp;
}
/**
 *  指定日期区域转时间戳
 * @param {array} timearea 时间区域
 */
function convertEnableAreaToTimestamp(timearea) {
  if (!timearea || !timearea.length) return;
  const start = timearea[0].split('-');
  const end = timearea[1].split('-');
  const startTimestamp = newDate(start[0], start[1], start[2]).getTime();
  const endTimestamp = newDate(end[0], end[1], end[2]).getTime();
  return {
    start,
    end,
    startTimestamp,
    endTimestamp
  };
}

/**
 *  指定特定日期数组转时间戳
 * @param {array} enableDays 指定时间数组
 */
function converEnableDaysToTimestamp(enableDays = []) {
  const enableDaysTimestamp = [];
  enableDays.forEach(item => {
    if (typeof item !== 'string') return warn('enableDays()入参日期格式错误');
    const tmp = item.split('-');
    if (tmp.length !== 3) return warn('enableDays()入参日期格式错误');
    const timestamp = newDate(tmp[0], tmp[1], tmp[2]).getTime();
    enableDaysTimestamp.push(timestamp);
  });
  return enableDaysTimestamp;
}

/**
 * 指定可选日期范围
 * @param {array} area 日期访问数组
 */
export const enableArea = (area = []) => {
  const self = currentPage;
  const { enableDays = [] } = self.data.calendar;
  let expectEnableDaysTimestamp = [];
  if (enableDays.length) {
    expectEnableDaysTimestamp = delRepeatedEnableDay(enableDays, area);
  }
  if (area.length === 2) {
    const {
      start,
      end,
      startTimestamp,
      endTimestamp
    } = convertEnableAreaToTimestamp(area);
    const startMonthDays = conf.getThisMonthDays(start[0], start[1]);
    const endMonthDays = conf.getThisMonthDays(end[0], end[1]);
    if (start[2] > startMonthDays || start[2] < 1) {
      return warn('enableArea() 开始日期错误,指定日期不在当前月份天数范围内');
    }
    if (start[1] > 12 || start[1] < 1) {
      return warn('enableArea() 开始日期错误,月份超出1-12月份');
    }
    if (end[2] > endMonthDays || end[2] < 1) {
      return warn('enableArea() 截止日期错误,指定日期不在当前月份天数范围内');
    }
    if (end[1] > 12 || end[1] < 1) {
      return warn('enableArea() 截止日期错误,月份超出1-12月份');
    }
    if (startTimestamp > endTimestamp) {
      warn('enableArea()参数最小日期大于了最大日期');
    } else {
      let { days = [], selectedDay = [] } = self.data.calendar;
      const daysCopy = days.slice();
      daysCopy.forEach(item => {
        const timestamp = newDate(item.year, item.month, item.day).getTime();
        if (
          (+startTimestamp > +timestamp || +timestamp > +endTimestamp) &&
          !expectEnableDaysTimestamp.includes(+timestamp)
        ) {
          item.disable = true;
          if (item.choosed) {
            item.choosed = false;
            selectedDay = selectedDay.filter(
              d =>
                `${item.year}-${item.month}-${item.day}` !==
                `${d.year}-${d.month}-${d.day}`
            );
          }
        } else if (item.disable) {
          item.disable = false;
        }
      });
      self.setData({
        'calendar.days': daysCopy,
        'calendar.selectedDay': selectedDay,
        'calendar.enableArea': area,
        'calendar.enableAreaTimestamp': [startTimestamp, endTimestamp]
      });
    }
  } else {
    warn('enableArea()参数需为时间范围数组,形如:["2018-8-4" , "2018-8-24"]');
  }
};
/**
 * 指定特定日期可选
 * @param {array} days 指定日期数组
 */
export function enableDays(days = []) {
  const self = currentPage;
  const { enableArea = [], enableAreaTimestamp = [] } = self.data.calendar;
  let expectEnableDaysTimestamp = [];
  if (enableArea.length) {
    expectEnableDaysTimestamp = delRepeatedEnableDay(days, enableArea);
  } else {
    expectEnableDaysTimestamp = converEnableDaysToTimestamp(days);
  }
  let { days: allDays = [], selectedDay = [] } = self.data.calendar;
  const daysCopy = allDays.slice();
  daysCopy.forEach(item => {
    const timestamp = newDate(item.year, item.month, item.day).getTime();
    let setDisable = false;
    if (enableAreaTimestamp.length) {
      if (
        (+enableAreaTimestamp[0] > +timestamp ||
          +timestamp > +enableAreaTimestamp[1]) &&
        !expectEnableDaysTimestamp.includes(+timestamp)
      ) {
        setDisable = true;
      }
    } else if (!expectEnableDaysTimestamp.includes(+timestamp)) {
      setDisable = true;
    }
    if (setDisable) {
      item.disable = true;
      if (item.choosed) {
        item.choosed = false;
        selectedDay = selectedDay.filter(
          d =>
            `${item.year}-${item.month}-${item.day}` !==
            `${d.year}-${d.month}-${d.day}`
        );
      }
    } else {
      item.disable = false;
    }
  });
  self.setData({
    'calendar.days': daysCopy,
    'calendar.selectedDay': selectedDay,
    'calendar.enableDays': days,
    'calendar.enableDaysTimestamp': expectEnableDaysTimestamp
  });
}

export default (config = {}) => {
  tips(
    '使用中若遇问题请反馈至 https://github.com/treadpit/wx_calendar/issues ✍️'
  );
  const weeksCh = ['日', '一', '二', '三', '四', '五', '六'];
  currentPage = getCurrentPage();
  currentPage.config = config;
  currentPage.setData({
    'calendar.weeksCh': weeksCh
  });
  if (config.defaultDay && typeof config.defaultDay === 'string') {
    const day = config.defaultDay.split('-');
    if (day.length < 3) {
      return warn('配置 jumpTo 格式应为: 2018-4-2 或 2018-04-02');
    }
    jump(+day[0], +day[1], +day[2]);
  } else {
    conf.jumpToToday.call(currentPage);
  }
};

2. 在需要使用的界面

wxml

<calendar calendar="{{calendar}}" gesture="{{gesture}}"></calendar>

js

import initCalendar from '../../component/calendar/main.js';

const conf = {
  onShow: function() {
    initCalendar();
  }
};

Page(conf);

 

效果图

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值