纯手写时间间隔组件
需求:小程序中可以根据时间段进行选择开始时间和结束时间,如:当前时间是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-popup
customStyle="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">
<view
style="{{ 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">
<view
style="{{ computed.wrapperStyle({ offset:endOptions.offset, itemHeight, visibleItemCount, duration:endOptions.duration }) }}">
<view
class="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
} = target
const 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.type
this.setData({
[`${options}.startY`]: event.touches[0].clientY,
[`${options}.startOffset`]: this.data[options].offset,
[`${options}.duration`]: 0,
});
},
onTouchMove(event) {
const options = event.currentTarget.dataset.type
const 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.type
if (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.type
const {
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的时间选择器的样式代码,没办法时间紧迫写的样式达不到那种丝滑,只能凑合用