日历是咱们开发过程当中常常会使用到的一个功能,po主在开发小程序的过程当中就遇到一个场景须要使用日历组件。首先上网搜索一番,可是没有找到合适本身的,因而便决定本身写一款小程序日历组件。html
先上效果图:git
效果体验:github
这款日历组件主要是提供日期选择功能。首先是年月日,而后是时分秒。而且提供配置开始时间和结束时间设置,以及周末是否可选的设置项。web
整体来讲分为两大部分:小程序
要实现年月日选择功能,首先咱们使用下面代码中的 setMonthData 函数构造一个 7 * 6 的数据结构, 这个数据结构包含当月,以及部分上月和下月的天数。在构造的时候经过 isValidDay 函数判断当天是否能够选择,而后加上 valid 一个标识,以便在渲染的时候用于显示和点击判断。数据结构
构造完成的数据结构以下:函数
{ date: date, // 当天日期 dateStr: date.toString(), // 当天日期的字符串,渲染时候比较使用,因为小程序的wxml的语法限制,这里把日期转化成字符串进行比较 day: date.getDate(), // 当天日期的天数 valid: valid, // 当天日期是不是能够选择的 currentMonth: false // 当天日期是不是本月的日期 }
setMonthData 和 isValidDay 函数代码: this
// 根据条件判断当天日期是否有效 isValidDay(date) { let {startDate, endDate, needWeek} = this.data; let valid = false; if (startDate && endDate) { // 开始日期和结束日期都存在 if (date >= startDate && date < endDate) { valid = true; } } else if (startDate && !endDate) { // 开始日期存在结束日期不存在 if (date >= startDate) { valid = true; } } else if (!startDate && endDate) { // 开始日期不存在结束日期存在 if (date < endDate) { valid = true; } } else { valid = true; } if (!needWeek) { let weekDay = date.getDay(); if (weekDay === 0 || weekDay === 6) { valid = false; } } return valid; } // 根据传入的日期构造数据 setMonthData() { const currentDate = this.data.currentDate; let year = currentDate.getFullYear(), month = currentDate.getMonth(); // 当月全部天数的数据结构 let currentData = []; // 获取当月1号的星期0~6 let firstDayWeek = new Date(year, month, 1).getDay(); // 天数, 用来标识当前是本月中的哪一天 let dayIndex = 0; // 第1行 let firstCol = []; for (let i = 0; i < 7; i++) { if (i < firstDayWeek) { let date = new Date(year, month, dayIndex - (firstDayWeek - i) + 1); let valid = this.isValidDay(date); firstCol.push({ date: date, dateStr: date.toString(), day: date.getDate(), valid: valid, currentMonth: false }) } else { dayIndex += 1; let date = new Date(year, month, dayIndex); let valid = this.isValidDay(date); firstCol.push({ date: date, dateStr: date.toString(), day: dayIndex, valid: valid, currentMonth: true }); } } currentData.push(firstCol); // 第2~4行 for (let i = 0; i < 3; i++) { let col = []; for (let j = 0; j < 7; j++) { dayIndex += 1; let date = new Date(year, month, dayIndex); let valid = this.isValidDay(date); col.push({ date: date, dateStr: date.toString(), day: dayIndex, valid: valid, currentMonth: true }); } currentData.push(col); } // 第5行 let lastCol = []; // 余下一行中本月的天数 let restDay = new Date(year, month + 1, 0).getDate() - dayIndex; for (let i = 0; i < 7; i++) { if (i < restDay) { dayIndex += 1; let date = new Date(year, month, dayIndex); let valid = this.isValidDay(date); lastCol.push({ date: date, dateStr: date.toString(), day: dayIndex, valid: valid, currentMonth: true }); } else { let date = new Date(year, month + 1, i - restDay + 1); let valid = this.isValidDay(date); lastCol.push({ date: date, dateStr: date.toString(), day: date.getDate(), valid: valid, currentMonth: false }); } } currentData.push(lastCol); let restDay2 = restDay - 7; // 第6行 let lastCol2 = []; for (let i = 0; i < 7; i++) { if (i < restDay2) { dayIndex += 1; let date = new Date(year, month, dayIndex); let valid = this.isValidDay(date); lastCol2.push({ date: date, dateStr: date.toString(), day: dayIndex, valid: valid, currentMonth: true }); } else { let date = new Date(year, month + 1, i - restDay2 + 1); let valid = this.isValidDay(date); lastCol2.push({ date: date, dateStr: date.toString(), day: date.getDate(), valid: valid, currentMonth: false }); } } currentData.push(lastCol2); this.setData({ currentData: currentData }); }
构造完当月数据后咱们就能够在页面渲染这些数据,渲染的时候须要加一些判断条件:spa
class="cell {{td.currentMonth ? '' : 'otherMonth'}} {{td.valid ? '' : 'disabled'}} {{td.date && td.dateStr === selectedDateStr ? 'cur' : ''}}" 3d
currentMonth 控制当天日期显示是否为灰色, valid 控制当天日期是不是禁用状态,dateStr 用于和当天日期 selectedDateStr 进行比较以显示选中的日期状态
另外在每一个td 上的点击方法 chooseDate 中判断 valid 为 true后再进行日期的选择
上述部分相关代码:
// 选择日期方法 chooseDate(event) { let {i, j} = event.currentTarget.dataset; let td = this.data.currentData[i][j]; if(td.valid) { this.setSelectedDate(td.date); } } // 设置当天选中的日期 setSelectedDate(date) { if(!date) { this.setData({ selectedDate: null, selectedDateStr: null, selectedDateShow: null }); return; } else { const year = date.getFullYear(); const month = date.getMonth() + 1; const day = date.getDate(); const selectedDateShow = [year, month, day].map(this.fixZero).join('-'); this.setData({ selectedDate: date, selectedDateStr: date.toString(), selectedDateShow: selectedDateShow }); } }
//渲染代码 <view class='date-body' wx:if="{{mode==='date'}}"> <view class='tr'> <view class='th'><view class="cell">日</view></view> <view class='th'><view class="cell">一</view></view> <view class='th'><view class="cell">二</view></view> <view class='th'><view class="cell">三</view></view> <view class='th'><view class="cell">四</view></view> <view class='th'><view class="cell">五</view></view> <view class='th'><view class="cell">六</view></view> </view> <view class="tr" wx:for="{{currentData}}" wx:key="{{i}}" wx:for-item="tr" wx:for-index="i"> <view class="td" wx:for="{{tr}}" wx:key="{{j}}" wx:for-item="td" wx:for-index="j" data-i="{{i}}" data-j="{{j}}" bindtap="chooseDate"> <view hover-class="none" class="cell {{td.currentMonth ? '' : 'otherMonth'}} {{td.valid ? '' : 'disabled'}} {{td.date && td.dateStr === selectedDateStr ? 'cur' : ''}}"> {{td.day}} </view> </view> </view> </view>
//样式代码 .picker .date-body .tr > .td > .cell { display: inline-block; width: 60rpx; height: 60rpx; line-height: 60rpx; margin: 6rpx; cursor: pointer; border-radius: 50%; -webkit-tap-highlight-color: rgba(0, 0, 0, 0); color: #333333; } .picker .date-body .tr > .td > .cell.otherMonth { color: #999999; } .picker .date-body .tr > .td > .cell.cur { background-color: #ff4158 !important; color: #ffffff; } .picker .date-body .tr > .td > .cell.disabled { color: #cccccc; background: #efefef; cursor: not-allowed; }
至此这个页面的功能基本完成了,可是顶部还有一些功能箭头,用于切换下月,下一年,以及上月,上一年。
这个功能咱们经过下面的changeDate方法来实现。首先这个函数中先根据当前事件 type 来判断是 增长一年(月),或者是减小一年(月)。而后对当月的日期 currentDate 进行从新构造,
构造完成后调用 setCurrentDate 方法从新设置当月的日期,最后再调用 setMonthData 从新渲染日期列表数据。
上述部分相关代码:
// 改变日期 changeDate(event) { let currentDate = this.data.currentDate; let year = currentDate.getFullYear(), month = currentDate.getMonth(); let type = event.currentTarget.dataset.type; switch (type) { case 'year-': currentDate.setFullYear(year - 1) break; case 'year+': currentDate.setFullYear(year + 1) break; case 'month-': currentDate.setMonth(month - 1) break; case 'month+': currentDate.setMonth(month + 1) break; } this.setCurrentDate(currentDate); this.setMonthData(); } // 设置日期 setCurrentDate(date) { if(!date) { this.setData({ currentDate: null, currentDateStr: null }); } else { const year = date.getFullYear(); const month = date.getMonth() + 1; const dateStr = [year, month].map(this.fixZero).join('-'); this.setData({ currentDate: date, currentDateStr: dateStr }); } }
一开始po主使用的是 scroll-view 来实现时间选择功能的。须要经过三个 scroll-view 进行联动,监听每一个 scroll-view 的 scroll 事件,而后根据 scrollTop 来判断当前选中的项。不过体验上仍是有所欠缺,一是 scroll-view 滑动会有滚动条出现,二是初次渲染时候若是给定一个初始值会明显看到 scroll-view 的滚动过程。
后来在小程序文档中找到 picker-view组件,这个组件能完美的解决上述的两个体验不佳的问题,并且使用很是简单。
picker-view 实现时分秒选择相关代码:
{ hours: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23], minutes: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 26, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59], seconds: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 26, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59], initTimeValue: [0, 0, 0], // 初始化选中时间的时,分,秒 timeValue: [0, 0, 0], // 选中时间的时,分,秒 }
<view class='time-body' wx:if="{{mode==='time'}}"> <picker-view indicator-class="selectItem" value="{{initTimeValue}}" bindchange="bindChange"> <picker-view-column> <view wx:for="{{hours}}" wx:key="*this" class="item">{{item}}时</view> </picker-view-column> <picker-view-column> <view wx:for="{{minutes}}" wx:key="*this" class="item">{{item}}分</view> </picker-view-column> <picker-view-column> <view wx:for="{{seconds}}" wx:key="*this" class="item">{{item}}秒</view> </picker-view-column> </picker-view> </view>
具体实现代码我放在github上了,感兴趣的同窗能够自取,若有不知足需求的地方能够尽情修改,有问题能够在评论区@我