小程序 - 日期選擇器

效果圖

 

實現步驟

1. 創建組件

wxml

<template name="datepicker">
	<view class="datepicker-bg" wx:if="{{showDatePicker}}" bindtap="closeDatePicker"></view>
	<input
		wx:if="{{showInput}}"
		class="datepicker-input"
		placeholder="{{placeholder}}"
		value="{{selectedValue || ''}}"
		type="text"
		bindinput="onInputDate"
		bindfocus="showDatepicker"/>
	<view wx:if="{{showDatePicker}}" class="datepicker-wrap flex box box-tb box-align-center">
		<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" catchtap="handleCalendar" 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">{{curYear || "--"}} 年 {{curMonth || "--"}} 月</view>
				<view class="next box box-lr" catchtap="handleCalendar" data-handle="next">
					<view class="next-handle box box-lr box-align-center box-pack-center">》</view>
				</view>
			</view>
			<view wx:if="{{weeksCh}}" class="weeks box box-lr box-pack-center box-align-center">
				<view class="flex week fs28" wx:for="{{weeksCh}}" wx:key="{{index}}" data-idx="{{index}}">{{item}}</view>
			</view>
			<view class="days box box-lr box-wrap" bindtouchstart="datepickerTouchstart" bindtouchmove="datepickerTouchmove">
				<view wx:if="{{empytGrids}}" class="grid disable-day-color  box box-align-center box-pack-center"
          wx:for="{{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="{{days}}"
          wx:key="{{index}}"
          data-idx="{{index}}"
          data-disable="{{item.disable}}"
          catchtap="tapDayItem">
					<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>
				<view class="grid disable-day-color  box box-align-center box-pack-center"
          wx:for="{{lastEmptyGrids}}"
          wx:key="{{index}}"
          data-idx="{{index}}">
            <view class="day box box-align-center box-pack-center">{{item}}</view>
        </view>
			</view>
		</view>
	</view>
</template>

 

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;
}

.datepicker-bg {
  position: fixed;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
}

.datepicker-input {
  width: 300rpx;
  height: 50rpx;
  border: 1rpx solid #dadada;
  border-radius: 10rpx;
  padding: 10rpx;
  font-size: 28rpx;
}
/* stylelint-disable-next-line */
.datepicker-input::-webkit-input-placeholder {
  color: #dadada;
}

.datepicker-wrap {
  background-color: #fff;
  box-shadow: 0 0 10rpx 0 #dadada;
  position: relative;
}

.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: 50%;
  height: 80rpx;
  text-align: center;
}

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

.week {
  text-align: center;
}

.days {
  height: 500rpx;
}

.grid {
  width: 14.285714285714286%;
}

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

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

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

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

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

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

.pink-bg {
  background-color: #ff629a;
}

.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;
}

js

/**
 * 上滑
 * @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;
    }
  }
}

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  月份
   */
  calculateEmptyGrids(year, month) {
    conf.calculatePrevMonthGrids.call(this, year, month);
    conf.calculateNextMonthGrids.call(this, year, month);
  },
  /**
   * 計算上月應占的格子
   * @param {number} year 年份
   * @param {number} month  月份
   */
  calculatePrevMonthGrids(year, month) {
    const prevMonthDays = conf.getThisMonthDays(year, month - 1);
    const firstDayOfWeek = conf.getFirstDayOfWeek(year, month);
    let empytGrids = [];
    if (firstDayOfWeek > 0) {
      const len = prevMonthDays - firstDayOfWeek;
      for (let i = prevMonthDays; i > len; i--) {
        empytGrids.push(i);
      }
      this.setData({
        'datepicker.empytGrids': empytGrids.reverse()
      });
    } else {
      this.setData({
        'datepicker.empytGrids': null
      });
    }
  },
  /**
   * 計算下月應占的格子
   * @param {number} year 年份
   * @param {number} month  月份
   */
  calculateNextMonthGrids(year, month) {
    const thisMonthDays = conf.getThisMonthDays(year, month);
    const lastDayWeek = new Date(`${year}-${month}-${thisMonthDays}`).getDay();
    let lastEmptyGrids = [];
    if (+lastDayWeek !== 6) {
      const len = 7 - (lastDayWeek + 1);
      for (let i = 1; i <= len; i++) {
        lastEmptyGrids.push(i);
      }
      this.setData({
        'datepicker.lastEmptyGrids': lastEmptyGrids
      });
    } else {
      this.setData({
        'datepicker.lastEmptyGrids': null
      });
    }
  },
  /**
   * 設置日曆面板數據
   * @param {number} year 年份
   * @param {number} month  月份
   */
  calculateDays(year, month, curDate) {
    const { todayTimestamp } = this.data.datepicker;
    let days = [];
    let day;
    let selectMonth;
    let selectYear;
    const thisMonthDays = conf.getThisMonthDays(year, month);
    const selectedDay = this.data.datepicker.selectedDay;
    if (selectedDay && selectedDay.length) {
      day = selectedDay[0].day;
      selectMonth = selectedDay[0].month;
      selectYear = selectedDay[0].year;
    }
    for (let i = 1; i <= thisMonthDays; i++) {
      days.push({
        day: i,
        choosed: curDate
          ? i === curDate
          : year === selectYear && month === selectMonth && i === day,
        year,
        month
      });
    }
    days.map(item => {
      const timestamp = new Date(
        `${item.year}-${item.month}-${item.day}`
      ).getTime();
      if (this.config.disablePastDay && timestamp - todayTimestamp < 0) {
        item.disable = true;
      }
    });
    const tmp = {
      'datepicker.days': days
    };
    if (curDate) {
      tmp['datepicker.selectedDay'] = [
        {
          day: curDate,
          choosed: true,
          year,
          month
        }
      ];
    }
    this.setData(tmp);
  },
  /**
   * 跳轉至今天
   */
  jumpToToday() {
    const date = new Date();
    const curYear = date.getFullYear();
    const curMonth = date.getMonth() + 1;
    const curDate = date.getDate();
    conf.renderCalendar.call(this, curYear, curMonth, curDate);
  },
  /**
   * 渲染日曆
   * @param {number} year
   * @param {number} month
   * @param {number} day
   */
  renderCalendar(year, month, day) {
    const timestamp = new Date(`${year}-${month}-${day}`).getTime();
    this.setData({
      'datepicker.curYear': year,
      'datepicker.curMonth': month,
      'datepicker.todayTimestamp': timestamp
    });
    conf.calculateEmptyGrids.call(this, year, month);
    conf.calculateDays.call(this, year, month, day);
  },
  /**
   * 初始化日曆選擇器
   * @param {number} curYear
   * @param {number} curMonth
   * @param {number} curDate
   */
  init(curYear, curMonth, curDate) {
    const self = _getCurrentPage();
    const weeksCh = ['日', '一', '二', '三', '四', '五', '六'];
    self.setData({
      'datepicker.weeksCh': weeksCh,
      'datepicker.showDatePicker': true
    });
    if (!curYear && !curMonth && !curDate) return conf.jumpToToday.call(self);
    conf.renderCalendar.call(self, curYear, curMonth, curDate);
  },
  /**
   * 點擊輸入框調起日曆選擇器
   * @param {object} e  事件對象
   */
  showDatepicker(e) {
    const value = e.detail.value;
    if (value && typeof value === 'string') {
      const tmp = value.split('-');
      conf.init(+tmp[0], +tmp[1], +tmp[2]);
    } else {
      conf.init();
    }
  },
  /**
   * 當輸入日期時
   * @param {object} e  事件對象
   */
  onInputDate(e) {
    const self = _getCurrentPage();
    this.inputTimer && clearTimeout(this.inputTimer);
    this.inputTimer = setTimeout(() => {
      const v = e.detail.value;
      const _v = (v && v.split('-')) || [];
      const RegExpYear = /^\d{4}$/;
      const RegExpMonth = /^(([0]?[1-9])|([1][0-2]))$/;
      const RegExpDay = /^(([0]?[1-9])|([1-2][0-9])|(3[0-1]))$/;
      if (_v && _v.length === 3) {
        if (
          RegExpYear.test(_v[0]) &&
          RegExpMonth.test(_v[1]) &&
          RegExpDay.test(_v[2])
        ) {
          conf.renderCalendar.call(self, +_v[0], +_v[1], +_v[2]);
        }
      }
    }, 500);
  },
  /**
   * 計算當前日曆面板月份的前一月數據
   */
  choosePrevMonth() {
    const { curYear, curMonth } = this.data.datepicker;
    let newMonth = curMonth - 1;
    let newYear = curYear;
    if (newMonth < 1) {
      newYear = curYear - 1;
      newMonth = 12;
    }

    conf.calculateDays.call(this, newYear, newMonth);
    conf.calculateEmptyGrids.call(this, newYear, newMonth);

    this.setData({
      'datepicker.curYear': newYear,
      'datepicker.curMonth': newMonth
    });
  },
  /**
   * 計算當前日曆面板月份的後一月數據
   */
  chooseNextMonth() {
    const { curYear, curMonth } = this.data.datepicker;
    let newMonth = curMonth + 1;
    let newYear = curYear;
    if (newMonth > 12) {
      newYear = curYear + 1;
      newMonth = 1;
    }
    conf.calculateDays.call(this, newYear, newMonth);
    conf.calculateEmptyGrids.call(this, newYear, newMonth);

    this.setData({
      'datepicker.curYear': newYear,
      'datepicker.curMonth': newMonth
    });
  },
  /**
   * 切換月份
   * @param {!object} e 事件對象
   */
  handleCalendar(e) {
    const handle = e.currentTarget.dataset.handle;
    if (handle === 'prev') {
      conf.choosePrevMonth.call(this);
    } else {
      conf.chooseNextMonth.call(this);
    }
  },
  /**
   * 選擇具體日期
   * @param {!object} e  事件對象
   */
  tapDayItem(e) {
    const { idx, disable } = e.currentTarget.dataset;
    if (disable) return;
    const config = this.config;
    const { afterTapDay, onTapDay } = config;
    const { curYear, curMonth, days } = this.data.datepicker;
    const key = `datepicker.days[${idx}].choosed`;
    const selectedValue = `${curYear}-${curMonth}-${days[idx].day}`;
    if (this.config.type === 'timearea') {
      if (onTapDay && typeof onTapDay === 'function') {
        config.onTapDay(this.data.datepicker.days[idx], e);
        return;
      }
      this.setData({
        [key]: !days[idx].choosed
      });
    } else if (this.config.type === 'normal' && !days[idx].choosed) {
      const prev = days.filter(item => item.choosed)[0];
      const prevKey = prev && `datepicker.days[${prev.day - 1}].choosed`;
      if (onTapDay && typeof onTapDay === 'function') {
        config.onTapDay(days[idx], e);
        return;
      }
      const data = {
        [key]: true,
        'datepicker.selectedValue': selectedValue,
        'datepicker.selectedDay': [days[idx]]
      };
      if (prevKey) {
        data[prevKey] = false;
      }
      this.setData(data);
    }
    if (afterTapDay && typeof afterTapDay === 'function') {
      config.afterTapDay(days[idx]);
    }
  },
  /**
   * 關閉日曆選擇器
   */
  closeDatePicker() {
    this.setData({
      'datepicker.showDatePicker': false
    });
  },
  datepickerTouchstart(e) {
    const t = e.touches[0];
    const startX = t.clientX;
    const startY = t.clientY;
    this.slideLock = true; // 滑動事件加鎖
    this.setData({
      'gesture.startX': startX,
      'gesture.startY': startY
    });
  },
  datepickerTouchmove(e) {
    if (isLeftSlide.call(this, e)) {
      conf.chooseNextMonth.call(this);
    }
    if (isRightSlide.call(this, e)) {
      conf.choosePrevMonth.call(this);
    }
  }
};

function _getCurrentPage() {
  const pages = getCurrentPages();
  const last = pages.length - 1;
  return pages[last];
}

/**
 * 跳轉至今天
 */
export const jumpToToday = () => {
  const self = _getCurrentPage();
  conf.jumpToToday.call(self);
};

export default (config = {}) => {
  const self = _getCurrentPage();
  if (!config.type) config.type = 'normal';
  self.config = config;
  self.setData({
    datepicker: {
      showDatePicker: false,
      showInput: config.showInput === true || config.showInput === undefined,
      placeholder: config.placeholder || '請選擇日期'
    }
  });
  self.datepickerTouchstart = conf.datepickerTouchstart.bind(self);
  self.datepickerTouchmove = conf.datepickerTouchmove.bind(self);
  self.showDatepicker = conf.showDatepicker.bind(self);
  self.onInputDate = conf.onInputDate.bind(self);
  self.closeDatePicker = conf.closeDatePicker.bind(self);
  self.tapDayItem = conf.tapDayItem.bind(self);
  self.handleCalendar = conf.handleCalendar.bind(self);
};

/**
 * 獲取已選擇的日期
 */
export const getSelectedDay = () => {
  const self = _getCurrentPage();
  return self.data.datepicker.selectedDay;
};

 

2. 在需要使用的地方引入

wxml

<import src="../../template/datepicker/index.wxml"/>

<view class="datepicker-box">
	<!-- <button type="primary" bindtap="showDatepicker"> 點擊喚起日期選擇器 </button> -->
	<template is="datepicker" data="{{...datepicker}}" />
</view>

wxss

@import '../../template/datepicker/index.wxss';

.datepicker-box {
  margin: 100rpx;
}

button {
  margin-top: 100rpx;
}

 

js

import initDatepicker, {
  getSelectedDay,
  jumpToToday
} from '../../template/datepicker/index';
const conf = {
  onShow: function() {
    initDatepicker({
      // disablePastDay: true, // 是否禁選過去日期
      // showInput: false, // 默認爲 true
      // placeholder: '請選擇日期', // input 輸入框
      // type: 'normal', // [normal 普通單選模式(默認), timearea 時間段選擇模式(待開發), multiSelect 多選模式(待完善)]
      /**
       * 點擊日期後執行的事件
       * @param { object } currentSelect 當前點擊的日期
       */
      afterTapDay: currentSelect => {
        console.log('當前點擊的日期', currentSelect);
        console.log('getSelectedDay方法', getSelectedDay());
      }
      /**
       * 日期點擊事件(此事件會完全接管點擊事件)
       * @param { object } currentSelect 當前點擊的日期
       * @param {object} event 日期點擊事件對象
       */
      // onTapDay(currentSelect, event) {
      //   console.log(currentSelect);
      //   console.log(event);
      // },
    });
  },
  /**
   * 跳轉至今天
   */
  jump() {
    jumpToToday();
  }
};
Page(conf);

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章