手撸【菜鸟上门取件】时间选择器
站长
· 阅读数 22
背景: 近期做的项目有个需求是做一个类似菜鸟的取件时间选择器,去找了很久没找到合适的,没办法只能自己收撸,经过好几个小版本修改之后也算是定型了,这里总结一篇文档备忘,把源码贴出来后续方便后续copy
技术
uniapp + vue2 + uni-popup
兼容
因为目前我的项目只用到这三端,其他的都还没测,所以兼容不保证
- 支付宝小程序开发者工具popup弹出来会直接滚到最顶部,显示异常,但真机上面没问题,可以不用管
环境 | 兼容 |
---|---|
支付宝小程序 | ✔ |
微信小程序 | ✔ |
H5 | ✔ |
菜鸟上门时间选择器
需求分析:
1、弹窗从底部弹出
- 点击蒙层不可关闭
- 弹窗header左侧title , 右侧关闭按钮
2、左侧日期选择器
- 显示近3天日期
- 显示(今天、明天、周一、周二等)
3、右侧时间选择器
- 可选时间可配置
- 过期时间显示 “已过期”
- 选中效果
- 当前已无可选时间,应该删除今天日期,只可以选未来日期
代码实现:
1.popup弹窗
先做一下基础布局,简单的分成上左右三大块,并做一些基础的配置
<template>
<uni-popup
mask-background-color="rgba(0, 0, 0, .8)"
ref="datePickerPop"
type="bottom"
background-color="#fff"
:is-mask-click="false"
>
<view class="date_pop">
<view class="popup_header">
<view class="pop_title">请选择取件时间</view>
<view class="pop-close" @click="handleClose('datePop')" />
</view>
<!-- 日期 -->
<view class="date_con">
<scroll-view scroll-y="true" class="date_box">
</scroll-view>
<!-- 时间 -->
<scroll-view scroll-y="true" class="time_box">
</scroll-view>
</view>
</view>
</uni-popup>
</template>
<script>
export default {
name: 'TimePicker',
props: {
visible: {
required: true,
default: false
}
},
watch: {
visible(newVal) {
if (newVal) {
if (!this.selectedDate.date_zh) {
this.selectedDate = this.effectRecentDate[0];
}
this.$refs.datePickerPop.open();
} else {
this.$refs.datePickerPop.close();
}
}
},
methods: {
handleClose() {
this.$emit('update:visible', false);
},
}
};
</script>
<style scoped lang="scss">
.date_pop {
padding: 0;
height: 750rpx;
.popup_header {
display: flex;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
padding: 60rpx 40rpx;
.pop_title {
font-weight: bold;
font-size: 32rpx;
width: 90%;
}
.pop-close {
width: 60rpx;
height: 60rpx;
background: url('~@/static/images/close.png');
background-size: 22rpx;
background-position: center;
background-repeat: no-repeat;
}
}
.date_con {
font-size: 28rpx;
position: relative;
height: 600rpx;
}
.date_box {
position: absolute;
top: 0;
left: 0;
width: 40%;
height: 100%;
background: #f7f7f9;
overflow-y: scroll;
.date_item {
padding: 0 40rpx;
line-height: 100rpx;
}
}
.time_box {
position: absolute;
top: 0;
right: 0;
width: 60%;
height: 100%;
}
.date_active {
background: #fff;
}
}
</style>
2.日期+时间选择器
按照需求我重新设计了一下功能及交互
日期选择器
- 日期可配置,支持显示最近n天日期
- 显示今天、明天、后台及工作日
- 默认选中当日(今天)
时间选择器
基础功能
- 删除过期时间
- 今日所有可选日期都过期之后删除日期选框(今天)选项
- 选中时间后面打钩,并关闭弹窗
可选功能
- 显示已过期时间 (逻辑几个版本之前已经删除了,现在只剩类名,需要的同学可以大概看下代码把它加上或者评论区留个言我把给你找找代码 , 功能样式就类似菜鸟)
- 直接删除已过期时间
先看效果
🎉🎃核心逻辑:
1、生成左侧日期列表
// 生成时间选择器 最近n天的时间
/**
*@n {Number} : 生成的天数
*
*/
setRecentData(n) {
const oneDaySeconds = 60 * 1000 * 60 * 24;
const today = +new Date();
let list = [];
for (let i = 0; i < n; i++) {
let formatTime = this.formatTime_zh(today + oneDaySeconds * i);
list.push({
...formatTime,
week: i == 0 ? '今天' : i == 1 ? '明天' : formatTime.week
});
}
//设置一下默认选中日期
this.selectedDate = list[0];
return list;
},
// 时间处理函数
formatTime_zh(date){
date = new Date(date);
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const weekDay = date.getDay();
const formatNumber = (n) => {
n = n.toString();
return n[1] ? n : '0' + n;
};
const numToTxt = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
return {
date_zh: `${formatNumber(month)}月${formatNumber(day)}日`,
date_en: `${year}/${formatNumber(month)}/${formatNumber(day)}`,
week: numToTxt[weekDay]
};
},
最终数据格式如图:
2、判断时间有没有过期
因为考虑到取件没有那么快,至少要提前半小时下单,所以就有了下面的逻辑(我这里是90分钟)
- 传入 09:00-10:00 格式时间区间
- 截取过期时间, 和当前时间做对比
- 判断已过期 、即将过期 、未过期
/**
* @return {Number} 1:已过期 , 2:即将过期 , 3:未过期
* @time {String} 09:00-10:00
*/
checkRemainingMinute(time) {
if (!time) return;
//过期时间
const outTime = time.toString().split('-')[1];
// 这里兼容一下iphone,iphone不支持yyyy-mm-dd hh:mm 格式时间 ,分隔符换为 /
const fullYearDate = formatMinute(new Date(), '/');
const now = new Date(fullYearDate);
const dateTime = this.currentDate + ' ' + outTime;
const check = new Date(dateTime);
const difference = check - now;
const minutes = difference / (1000 * 60);
// minutes <= 0 : 已过期 --> 1
// minutes <= 90 : 即将过期 --> 2
// minutes > 0 : 未过期 --> 3
return minutes <= 0 ? 1 : minutes <= 90 ? 2 : 3;
}
/**
* @description yyyy-mm-dd hh:mm
* @author wangxinu
* @export
* @param {*} cent
* @returns
*/
formatMinute: (date, separator = '-') => {
date = new Date(date);
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const hour = date.getHours();
const minute = date.getMinutes();
const second = date.getSeconds();
const formatNumber = (n) => {
n = n.toString();
return n[1] ? n : '0' + n;
};
return `${formatNumber(year)}${separator}${formatNumber(month)}${separator}${formatNumber(
day,
)} ${formatNumber(hour)}:${formatNumber(minute)}`;
},
3、通过计算属性获取有效时间(即右侧列表展示即将过期的和未过期的时间)
data(){
return {
appointment: [
'08:00-09:00',
'09:00-10:00',
'10:00-11:00',
'11:00-12:00',
'12:00-13:00',
'13:00-14:00',
'14:00-15:00',
'15:00-16:00',
'16:00-17:00',
'17:00-18:00',
'18:00-19:00',
'19:00-20:00'
]
}
},
computed: {
// 有效取件时间
effectAppointmentTime() {
//取件时间列表
const appointment = this.appointment;
// 未来日期返回全部
if (this.selectedDate.date_en != this.currentDate) {
return appointment;
}
// 当日只返回有效时间
let list = appointment.filter((item) => this.checkRemainingMinute(item) != 1);
// 当天取件时间长度>0 添加立即上门
if (list.length > 0) {
list.unshift('立即上门');
}
return list;
}
},
4、通过计算属性获取有效日期
computed: {
// 有效日期
effectRecentDate() {
//查看有效时间列表
const effectAppointmentTime = this.effectAppointmentTime;
// 当日取件时间全部失效
if (effectAppointmentTime.length == 0) {
//删除(今日)
this.recentDateList.splice(0, 1);
//修改默认选中日期
this.selectedDate = this.recentDateList[0];
return this.recentDateList;
} else {
return this.recentDateList;
}
},
},
5、日期或时间选中函数
// 时间选择器修改函数
timeChange(date, type) {
const dateList = this.recentDateList;
if (type === 'date') {
// 选择日期
this.selectedDate = date;
this.selectedTime = '';
} else {
// 选择时间
this.selectedTime = date;
if (this.selectedDate.date_zh == '') {
this.selectedDate = dateList[0];
}
this.handleClose();
this.$emit('selectTime', this.selectedDate, this.selectedTime);
}
},
3.源码及使用
使用:
<template>
<div class="page">
<button @click="timePicker_visible = true" type="primary">打开弹窗</button>
<TimePicker :visible.sync="timePicker_visible" @selectTime="selectTime"/>
</div>
</template>
<script>
import TimePicker from './components/TimePicker';
export default {
name: 'test',
components: { TimePicker },
mixins: [],
props: {},
data() {
return {
timePicker_visible: false
};
},
methods:{
selectTime(date,time){
console.log('date',date)
console.log('time',time)
}
}
};
</script>
源码:
<template>
<uni-popup
mask-background-color="rgba(0, 0, 0, .8)"
ref="datePickerPop"
type="bottom"
background-color="#fff"
:is-mask-click="false"
>
<view class="date_pop">
<view class="popup-header">
<view class="pop-title">请选择取件时间</view>
<view class="pop-close" @click="handleClose('datePop')" />
</view>
<!-- 日期 -->
<view class="date_con">
<scroll-view :scroll-y="true" class="date_box" :show-scrollbar="false">
<view
v-for="date in effectRecentDate"
:key="date.date_zh"
:class="[`date_item`, selectedDate.date_zh == date.date_zh ? `date_active` : ``]"
@click="timeChange(date, 'date')"
>
{{ date.date_zh }}({{ date.week }})
</view>
</scroll-view>
<!-- 时间 -->
<scroll-view
class="time_box"
:scroll-y="true"
:scroll-with-animation="true"
:show-scrollbar="false"
>
<view
v-for="(time, index) in effectAppointmentTime"
:key="index"
:class="{
bottom: true,
time_item: true,
time_active: selectedTime === time
}"
@click="timeChange(effectAppointmentTime[index], `time`)"
>
{{ time }}
</view>
</scroll-view>
</view>
</view>
</uni-popup>
</template>
<script>
import dayjs from 'dayjs';
import cloneDeep from 'lodash/cloneDeep';
import { toFixed } from '@/common/js/utils';
export default {
name: 'TimePicker',
props: {
visible: {
required: true,
default: false
}
},
watch: {
visible(newVal) {
if (newVal) {
if (!this.selectedDate.date_zh) {
this.selectedDate = this.effectRecentDate[0];
}
this.setEffectAppointmentTime();
this.setEffectRecentDate();
this.$refs.datePickerPop.open();
} else {
this.$refs.datePickerPop.close();
}
}
},
created() {
this.setRecentData();
},
data() {
// 生成取件日期
this.toFixed = toFixed;
const defaultTimeList = [
'08:00-09:00',
'09:00-10:00',
'10:00-11:00',
'11:00-12:00',
'12:00-13:00',
'13:00-14:00',
'14:00-15:00',
'15:00-16:00',
'16:00-17:00',
'17:00-18:00',
'18:00-19:00',
'19:00-20:00'
];
return {
timeConfig: {
dayCount: 3,
/** 显示立即上门时间区间 */
startHour: 8,
endHour: 19
},
currentDate: dayjs().format('YYYY/MM/DD'),
selectedTime: '',
selectedDate: {},
recentDateList: [],
effectRecentDate: [],
effectAppointmentTime: defaultTimeList,
appointment: defaultTimeList
};
},
methods: {
// 有效日期
setEffectRecentDate() {
if (this.effectAppointmentTime.length > 0) {
this.effectRecentDate = cloneDeep(this.recentDateList);
return;
}
let list = this.recentDateList;
list.splice(0, 1);
// 当日取件时间全部失效
this.effectRecentDate = list;
this.selectedDate = this.recentDateList[0];
this.setEffectAppointmentTime();
},
// 有效取件时间
setEffectAppointmentTime() {
const appointment = this.appointment;
// 未来日期返回全部
if (this.selectedDate.date_en != this.currentDate) {
this.effectAppointmentTime = appointment;
return;
}
// 当日
let list = appointment.filter((item) => this.checkRemainingMinute(item) === 3);
const { startHour, endHour } = this.timeConfig;
// 当日只返回有效时间
if (new Date().getHours() >= startHour && new Date().getHours() < endHour) {
list.unshift('立即上门');
}
this.effectAppointmentTime = list;
},
handleClose() {
this.$emit('update:visible', false);
this.$emit('update:showPopup', false);
},
// 生成时间选择器 最近n天的时间
setRecentData(n = this.timeConfig.dayCount) {
const oneDayTime = 60 * 1000 * 60 * 24;
const today = +new Date();
let list = [];
for (let i = 0; i < n; i++) {
let formatTime = this.formatTime_zh(today + oneDayTime * i);
list.push({
...formatTime,
week: i == 0 ? '今天' : i == 1 ? '明天' : i == 2 ? '后天' : formatTime.week
});
}
this.selectedDate = list[0];
this.recentDateList = list;
},
// 时间处理函数
formatTime_zh: (date) => {
date = new Date(date);
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const weekDay = date.getDay();
const formatNumber = (n) => {
n = n.toString();
return n[1] ? n : '0' + n;
};
const numToTxt = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
return {
date_zh: `${formatNumber(month)}月${formatNumber(day)}日`,
date_en: `${year}/${formatNumber(month)}/${formatNumber(day)}`,
week: numToTxt[weekDay]
};
},
// 时间选择器修改函数
timeChange(date, type) {
const dateList = this.recentDateList;
if (type === 'date') {
// 选择日期
this.selectedDate = date;
this.selectedTime = '';
this.setEffectAppointmentTime();
} else {
// 选择时间
if (this.selectedDate.date_zh == '') {
this.selectedDate = dateList[0];
}
this.selectedTime = date;
this.handleClose();
}
this.$emit('selectTime', this.selectedDate, this.selectedTime);
},
/**
* @return {Number} 1:已过期 , 2:即将过期 , 3:未过期
*/
checkRemainingMinute(time) {
if (!time) return;
const outTime = time.toString().split('-')[0];
const now = dayjs().valueOf();
const dateTime = this.currentDate + ' ' + outTime;
const check = dayjs(dateTime).valueOf();
const difference = check - now;
const minutes = difference / (1000 * 60);
// minutes <= 0 : 已过期 --> 1
// minutes <= 90 : 即将过期 --> 2
// minutes > 0 : 未过期 --> 3
return minutes <= 0 ? 1 : minutes <= 60 ? 2 : 3;
}
}
};
</script>
<style scoped lang="scss">
.date_pop {
padding: 0;
height: 750rpx;
.popup-header {
display: flex;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
padding: 60rpx 40rpx;
.pop-title {
font-weight: bold;
font-size: 32rpx;
width: 90%;
}
.pop-close {
width: 60rpx;
height: 60rpx;
background: url('~@/static/images/close.png');
background-size: 22rpx;
background-position: center;
background-repeat: no-repeat;
}
}
.date_con {
font-size: 28rpx;
position: relative;
height: 600rpx;
}
.date_box {
position: absolute;
top: 0;
left: 0;
width: 40%;
height: 600rpx;
background: #f7f7f9;
overflow-y: scroll;
.date_item {
padding: 0 40rpx;
line-height: 100rpx;
}
.date_active {
background: #fff;
}
}
.time_box {
position: absolute;
top: 0;
right: 0;
width: 60%;
height: 600rpx;
.disabled {
color: #ccc;
&::after {
content: '已过期';
margin-left: 130rpx;
}
}
.outTime {
color: #ccc;
&::after {
content: '即将过期';
margin-left: 100rpx;
}
}
.time_item {
padding: 0 40rpx;
line-height: 100rpx;
}
}
.time_active {
color: #ff5b29;
position: relative;
&::after {
position: absolute;
content: '✔';
right: 15%;
margin: auto;
}
}
}
</style>
4.TODO:
- 时间区域打开显示对应选中时间位置
- 右侧时间列表改后台返回