纯手写时间间隔组件
需求:小程序中可以根据时间段进行选择开始时间和结束时间,如:当前时间是09:00,
则我可以从9点开始选择时间,每半个小时为间隔,那么下一个时间就算9:30,10:00,依次类推
就像element-ui中有个时间选择器就可以根据自己设置的时间间隔去选择,可以0.5小时,1小时或者2.5小时的间隔进行选择
由于公司小程序用的是vant-ui来开发的,我去找vant的时间组件,发现没有这样的,所以决定自己开发一下这个组件
!!!注意:
由于公司用的是vant,所以弹窗按钮用的都是我们用vant自己封装的组件,
在这里就不展示弹窗组件还有按钮了,弹窗和按钮就得大家自己用自己的框架去显示了
看效果
间隔一小时
提前30分钟和推迟30分钟 当前时间10:51
创建 time-interval组件 如果你想全局使用则在app.json的usingComponents中添加你得组件地址
"usingComponents": {"time-interval": "/components/time-interval/time-interval"}
创建好后就开始编码 HTML部分
<import src="../../static/templates/template.wxml"></import>
<wxs src="./util.wxs" module="computed" />
<custom-popupcustomStyle="width: 100%;max-height: calc(100% - 148rpx); overflow: hidden; border-radius: 24rpx 24rpx 0 0;"hidden="{{!isShowTime}}" isShowPopup="{{isShowTime}}" title="{{title}}"isCloseOnClickOverlay="{{isCloseOnClickOverlay}}" bindhidePopup="hidePopup" bind:titleTap="titleTap">
<view class="w-time_container"><view class="w-t_left" data-type="startOptions" bind:touchstart="onTouchStart" catch:touchmove="onTouchMove"bind:touchend="onTouchEnd" bind:touchcancel="onTouchEnd"><viewstyle="{{ computed.wrapperStyle({ offset:startOptions.offset, itemHeight, visibleItemCount, duration:startOptions.duration }) }}"><view class="w-time_row {{index===startOptions.currentIndex?'w-check_box':''}}"style="height: {{ itemHeight }}px" wx:for="{{timeList}}" wx:key="index" data-item="{{item}}"data-index="{{ index }}" data-type="startOptions" bind:tap="onClickItem">{{item.time}}</view></view></view><view class="w-t_right" data-type="endOptions" bind:touchstart="onTouchStart" catch:touchmove="onTouchMove"bind:touchend="onTouchEnd" bind:touchcancel="onTouchEnd"><viewstyle="{{ computed.wrapperStyle({ offset:endOptions.offset, itemHeight, visibleItemCount, duration:endOptions.duration }) }}"><viewclass="w-time_row {{index===endOptions.currentIndex?'w-check_box':''}} {{startTime.timeStamp>=item.timeStamp?'w-disabled':''}}"wx:for="{{timeList}}" wx:key="index" data-item="{{item}}" data-index="{{ index }}"data-type="endOptions" bind:tap="{{startTime.timeStamp>=item.timeStamp?'':'onClickItem'}}">{{item.time}}</view></view></view><view class="w-pick_mask" style="background-size: 100% 40%;"></view></view><template is="iconBtn" data="{{btnText: '确定', btnTap: 'confirmTime'}}"></template>
</custom-popup>
js部分
function getCurrentDate(param = 's', target = '') {var now = target ? new Date(toIosDate(target)) : new Date();var year = now.getFullYear(); var month = now.getMonth(); var date = now.getDate(); var day = now.getDay(); var hour = now.getHours(); var minu = now.getMinutes(); var sec = now.getSeconds(); month = month + 1;if (month < 10) month = "0" + month;if (date < 10) date = "0" + date;if (hour < 10) hour = "0" + hour;if (minu < 10) minu = "0" + minu;if (sec < 10) sec = "0" + sec;const arr = {'Y': year,'M': year + "-" + month,'D': year + "-" + month + "-" + date,'h': year + "-" + month + "-" + date + " " + hour,'m': year + "-" + month + "-" + date + " " + hour + ":" + minu,'s': year + "-" + month + "-" + date + " " + hour + ":" + minu + ":" + sec}return {year,month,day: date,hour,minu,sec,date: arr[param]};
}function range(num, min, max) {return Math.min(Math.max(num, min), max);
}function isObj(x) {const type = typeof x;return x !== null && (type === 'object' || type === 'function');
}
const DEFAULT_DURATION = 200;
let app = getApp();
let isIOS = /ios/ig.test(app.globalData.systemInfo.system);
function toIosDate(date) {return isIOS ? date.replace(/-/g, '/') : date
}
Component({options: {addGlobalClass: true,styleIsolation: "apply-shared"},properties: {isShowTime: {type: Boolean,value: false,observer: 'initArr'},title: {type: String,value: '选择时间'},isCloseOnClickOverlay: {type: Boolean,value: false},intervalTime: {type: Number,value: 1/60, },hasDelay: {type: Number,value: 0, },curDate: {type: String,valeu: getCurrentDate('D').date},visibleItemCount: {type: Number,value: 6},itemHeight: {type: Number,value: 44}},data: {startTime: {},endTime: {},timeList: [],startOptions: {startY: 0,startOffset: 0,duration: 0,offset: 0,currentIndex: 0},endOptions: {startY: 0,startOffset: 0,duration: 0,offset: 0,currentIndex: 0}},methods: {initArr(nv, ov) {if (nv) {const {date,hour,minu,} = getCurrentDate('D')const w_date = this.properties.curDate ? (date === this.properties.curDate ? '' : `${this.properties.curDate} 00:00`) : `${date} ${hour}:${minu}`this.setData({timeList: this.createTime(getCurrentDate('D', w_date), this.properties.intervalTime)})this.setIndex(0, 'startOptions');this.setIndex(1, 'endOptions');}},createTime(target, h = 1) {const {hour,minu,date} = targetconst e_date = this.getRecentDate(1, this.properties.curDate)let arr = []let startStamp = new Date(toIosDate(date + ` ${hour}:${minu}`)).getTime()if (Math.abs(this.properties.hasDelay) > 0) {startStamp = startStamp + this.properties.hasDelay*60*1000} const endStamp = new Date(toIosDate(e_date + ' 00:00')).getTime()for (let i = startStamp; i < endStamp; i += (h * 60 * 60 * 1000)) {const res = this.formatDate(i)arr.push({time: res.time,date: res.date,dateM: res.dateM,timeStamp: i,disabled: false})}return arr},confirmTime() {if (!this.data.endTime.timeStamp) {return wx.showToast({title: '请选择结束时间',icon: 'none',})}this.triggerEvent('chooseInterver', {startTime: this.data.startTime,endTime: this.data.endTime})},formatDate(target) {const date = new Date(target)let year = date.getFullYear();let months = date.getMonth() + 1;let month = (months < 10 ? '0' + months : months).toString();let day = date.getDate() < 10 ? '0' + date.getDate() : date.getDate();let hours = date.getHours() < 10 ? '0' + date.getHours() : date.getHours();let min = date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes();let sec = date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds();return {year: year.toString(),month,day,hours,min,sec,time: hours + ":" + min,date: year + "-" + month + "-" + day + " " + hours + ":" + min,dateM: year + "-" + month + "-" + day}},getRecentDate(day, target) {var date1 = target ? new Date(toIosDate(target)) : new Date(),time1 = date1.getFullYear() + "-" + (date1.getMonth() + 1) + "-" + date1.getDate(); var date2 = new Date(date1);date2.setDate(date1.getDate() + day);const y = date2.getFullYear();const m = (date2.getMonth() + 1) > 9 ? (date2.getMonth() + 1) : '0' + (date2.getMonth() + 1)const d = date2.getDate() > 9 ? date2.getDate() : '0' + date2.getDate()let h = date2.getHours() < 10 ? '0' + date2.getHours() : date2.getHours();let n = date2.getMinutes() < 10 ? '0' + date2.getMinutes() : date2.getMinutes();let s = date2.getSeconds() < 10 ? '0' + date2.getSeconds() : date2.getSeconds();return y + "-" + m + "-" + d;},hidePopup() {this.setData({[`startOptions.startY`]: 0,[`startOptions.startOffset`]: 0,[`startOptions.duration`]: 0,[`startOptions.currentIndex`]: 0,[`endOptions.startY`]: 0,[`endOptions.startOffset`]: 0,[`endOptions.duration`]: 0,[`endOptions.currentIndex`]: 0,})this.triggerEvent('close')},titleTap() {this.triggerEvent('titleTap')},getCount() {return this.data.timeList.length;},onTouchStart(event) {const options = event.currentTarget.dataset.typethis.setData({[`${options}.startY`]: event.touches[0].clientY,[`${options}.startOffset`]: this.data[options].offset,[`${options}.duration`]: 0,});},onTouchMove(event) {const options = event.currentTarget.dataset.typeconst deltaY = event.touches[0].clientY - this.data[options].startY;this.setData({[`${options}.offset`]: range(this.data[options].startOffset + deltaY,-(this.getCount() * this.properties.itemHeight),this.properties.itemHeight),});},onTouchEnd(event) {const options = event.currentTarget.dataset.typeif (this.data[options].offset !== this.data[options].startOffset) {this.setData({[`${options}.duration`]: DEFAULT_DURATION});const index = range(Math.round(-this.data[options].offset / this.data.itemHeight),0,this.getCount() - 1);this.setIndex(index, options);}},onClickItem(event) {const options = event.currentTarget.dataset.typeconst {index} = event.currentTarget.dataset;this.setIndex(index, options);},setIndex(index, options) {const {data} = this;index = this.adjustIndex(index) || 0;const offset = -index * this.properties.itemHeight;if (index !== data[options].currentIndex) {this.setData({[`${options}.offset`]: offset,[`${options}.currentIndex`]: index,[`${options==='startOptions'?'startTime':'endTime'}`]: this.data.timeList[index]})if (options === 'endOptions') {if (this.data.startTime.timeStamp < this.data.timeList[index].timeStamp) {this.setData({endTime: this.data.timeList[index]})} else {this.setData({endTime: {}})}}else{this.setData({endTime: {}})}} else {this.setData({[`${options}.offset`]: offset,[`${options==='startOptions'?'startTime':'endTime'}`]: this.data.timeList[index]});}},adjustIndex(index) {const count = this.getCount();index = range(index, 0, count);for (let i = index; i < count; i++) {if (!this.isDisabled(this.data.timeList[i])) return i;}for (let i = index - 1; i >= 0; i--) {if (!this.isDisabled(this.data.timeList[i])) return i;}},isDisabled(option) {return isObj(option) && option.disabled;},}
})
css 部分
.w-time_container {position: relative;height: 600rpx;width: 100%;padding: 8rpx;box-sizing: border-box;display: flex;
}.w-t_left,
.w-t_right {width: 50%;height: 100%;display: flex;padding: 48rpx 0;flex-direction: column;align-items: center;box-sizing: border-box;overflow: hidden;
}.w-pick_mask{position: absolute;top: 0;left: 0;z-index: 1;width: 100%;height: 100%;background-image: linear-gradient(180deg, hsla(0, 0%, 100%, 0.9), hsla(0, 0%, 100%, 0.4)), linear-gradient(0deg, hsla(0, 0%, 100%, 0.9), hsla(0, 0%, 100%, 0.4));background-repeat: no-repeat;background-position: top, bottom;transform: translateZ(0);pointer-events: none;
}.w-time_row {width: 100%;display: flex;align-items: center;justify-content: center;font-size: 32rpx;font-weight: 600;box-sizing: border-box;
}.w-check_box {color: #264AFF;
}
.w-disabled {color: #0A1B33B3;
}
工具类util.wxs
function addUnit(value) {if (value == null) {return undefined;}var REGEXP1 = getRegExp('^-?\d+(\.\d+)?$');return REGEXP1.test('' + value) ? value + 'px' : value;
}
var REGEXP2 = getRegExp('{|}|"', 'g');function keys(obj) {return JSON.stringify(obj).replace(REGEXP2, '').split(',').map(function (item) {return item.split(':')[0];});
}function kebabCase(word) {var newWord = word.replace(getRegExp("[A-Z]", 'g'), function (i) {return '-' + i;}).toLowerCase()return newWord;
}function isArray(array) {return array && array.constructor === 'Array';
}function style(styles) {if (isArray(styles)) {return styles.filter(function (item) {return item != null && item !== '';}).map(function (item) {return style(item);}).join(';');}if ('Object' === styles.constructor) {return keys(styles).filter(function (key) {return styles[key] != null && styles[key] !== '';}).map(function (key) {return [kebabCase(key), [styles[key]]].join(':');}).join(';');}return styles;
}function wrapperStyle(data) {var offset = addUnit(data.offset + (data.itemHeight * (data.visibleItemCount - 1)) / 2);return style({transition: 'transform ' + data.duration + 'ms','line-height': addUnit(data.itemHeight),transform: 'translate3d(0, ' + offset + ', 0)',});
}module.exports = {wrapperStyle: wrapperStyle,
};
Attributes
参数 | 说明 | 类型 | 可选值 | 默认值 |
---|
intervalTime | 间隔时间(单位小时) | Number | 1/60(一分钟);30/60(30分钟); 1(1小时);2.5(两个半小时) | 1/60 |
hasDelay | 是否是推迟还是提前多少分钟 提前则大于0,推迟则小于0,且不能大于一天 | Number | 10(提前10分钟);-10(推迟10分钟) | 0 |
curDate | 选择的日期,默认当天的日期 | String | 2023-02-11或 2023-02-11 09:00:00 | 当天时间 |
Events
参数 | 说明 | 类型 | 参数 | 返回值 |
---|
chooseInterver | 选择时间结果 | Function | - | {starTime:resultTime,endTime:resultTime} |
resultTime
prop | 说明 |
---|
date | 日期+时分 2023-09-13 09:33 |
dateM | 日期没有时分 2023-09-13 |
time | 选择的时分 09:33 |
timeStamp | 选择date的时间戳 1694568780000 |
看过vant源码的会发现 我这里会有一部分vant的代码,确实是,我用了一下vant的时间选择器的样式代码,没办法时间紧迫写的样式达不到那种丝滑,只能凑合用