uni-popup手寫菜鳥上門取件時(shí)間選擇器
引言
近期做的項(xiàng)目有個(gè)需求是做一個(gè)類似菜鳥的取件時(shí)間選擇器,去找了很久沒找到合適的,沒辦法只能自己收擼,經(jīng)過(guò)好幾個(gè)小版本修改之后也算是定型了,這里總結(jié)一篇文檔備忘,把源碼貼出來(lái)后續(xù)方便后續(xù)copy
技術(shù)
uniapp + vue2 + uni-popup
兼容
因?yàn)槟壳拔业捻?xiàng)目只用到這三端,其他的都還沒測(cè),所以兼容不保證
- 支付寶小程序開發(fā)者工具popup彈出來(lái)會(huì)直接滾到最頂部,顯示異常,但真機(jī)上面沒問題,可以不用管
| 環(huán)境 | 兼容 |
|---|---|
| 支付寶小程序 | ? |
| 微信小程序 | ? |
| H5 | ? |
菜鳥上門時(shí)間選擇器

需求分析:
1、彈窗從底部彈出
- 點(diǎn)擊蒙層不可關(guān)閉
- 彈窗header左側(cè)title , 右側(cè)關(guān)閉按鈕
2、左側(cè)日期選擇器
- 顯示近3天日期
- 顯示(今天、明天、周一、周二等)
3、右側(cè)時(shí)間選擇器
- 可選時(shí)間可配置
- 過(guò)期時(shí)間顯示 “已過(guò)期”
- 選中效果
- 當(dāng)前已無(wú)可選時(shí)間,應(yīng)該刪除今天日期,只可以選未來(lái)日期
代碼實(shí)現(xiàn):
1.popup彈窗
先做一下基礎(chǔ)布局,簡(jiǎn)單的分成上左右三大塊,并做一些基礎(chǔ)的配置

<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">請(qǐng)選擇取件時(shí)間</view>
<view class="pop-close" @click="handleClose('datePop')" />
</view>
<!-- 日期 -->
<view class="date_con">
<scroll-view scroll-y="true" class="date_box">
</scroll-view>
<!-- 時(shí)間 -->
<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.日期+時(shí)間選擇器
按照需求我重新設(shè)計(jì)了一下功能及交互
日期選擇器
- 日期可配置,支持顯示最近n天日期
- 顯示今天、明天、后臺(tái)及工作日
- 默認(rèn)選中當(dāng)日(今天)
時(shí)間選擇器
基礎(chǔ)功能
- 刪除過(guò)期時(shí)間
- 今日所有可選日期都過(guò)期之后刪除日期選框(今天)選項(xiàng)
- 選中時(shí)間后面打鉤,并關(guān)閉彈窗
可選功能
- 顯示已過(guò)期時(shí)間 (邏輯幾個(gè)版本之前已經(jīng)刪除了,現(xiàn)在只剩類名,需要的同學(xué)可以大概看下代碼把它加上或者評(píng)論區(qū)留個(gè)言我把給你找找代碼 , 功能樣式就類似菜鳥)
- 直接刪除已過(guò)期時(shí)間
先看效果

????核心邏輯:
1、生成左側(cè)日期列表
// 生成時(shí)間選擇器 最近n天的時(shí)間
/**
*@n {Number} : 生成的天數(shù)
*
*/
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
});
}
//設(shè)置一下默認(rèn)選中日期
this.selectedDate = list[0];
return list;
},
// 時(shí)間處理函數(shù)
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]
};
},
最終數(shù)據(jù)格式如圖:

2、判斷時(shí)間有沒有過(guò)期
因?yàn)榭紤]到取件沒有那么快,至少要提前半小時(shí)下單,所以就有了下面的邏輯(我這里是90分鐘)
- 傳入 09:00-10:00 格式時(shí)間區(qū)間
- 截取過(guò)期時(shí)間, 和當(dāng)前時(shí)間做對(duì)比
- 判斷已過(guò)期 、即將過(guò)期 、未過(guò)期
/**
* @return {Number} 1:已過(guò)期 , 2:即將過(guò)期 , 3:未過(guò)期
* @time {String} 09:00-10:00
*/
checkRemainingMinute(time) {
if (!time) return;
//過(guò)期時(shí)間
const outTime = time.toString().split('-')[1];
// 這里兼容一下iphone,iphone不支持yyyy-mm-dd hh:mm 格式時(shí)間 ,分隔符換為 /
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 : 已過(guò)期 --> 1
// minutes <= 90 : 即將過(guò)期 --> 2
// minutes > 0 : 未過(guò)期 --> 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、通過(guò)計(jì)算屬性獲取有效時(shí)間(即右側(cè)列表展示即將過(guò)期的和未過(guò)期的時(shí)間)
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: {
// 有效取件時(shí)間
effectAppointmentTime() {
//取件時(shí)間列表
const appointment = this.appointment;
// 未來(lái)日期返回全部
if (this.selectedDate.date_en != this.currentDate) {
return appointment;
}
// 當(dāng)日只返回有效時(shí)間
let list = appointment.filter((item) => this.checkRemainingMinute(item) != 1);
// 當(dāng)天取件時(shí)間長(zhǎng)度>0 添加立即上門
if (list.length > 0) {
list.unshift('立即上門');
}
return list;
}
},
4、通過(guò)計(jì)算屬性獲取有效日期
computed: {
// 有效日期
effectRecentDate() {
//查看有效時(shí)間列表
const effectAppointmentTime = this.effectAppointmentTime;
// 當(dāng)日取件時(shí)間全部失效
if (effectAppointmentTime.length == 0) {
//刪除(今日)
this.recentDateList.splice(0, 1);
//修改默認(rèn)選中日期
this.selectedDate = this.recentDateList[0];
return this.recentDateList;
} else {
return this.recentDateList;
}
},
},
5、日期或時(shí)間選中函數(shù)
// 時(shí)間選擇器修改函數(shù)
timeChange(date, type) {
const dateList = this.recentDateList;
if (type === 'date') {
// 選擇日期
this.selectedDate = date;
this.selectedTime = '';
} else {
// 選擇時(shí)間
this.selectedTime = date;
if (this.selectedDate.date_zh == '') {
this.selectedDate = dateList[0];
}
this.handleClose();
this.$emit('selectTime', this.selectedDate, this.selectedTime);
}
},
源碼及使用
使用:
<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">請(qǐng)選擇取件時(shí)間</view>
<view class="pop-close" @click="handleClose('datePop')" />
</view>
<!-- 日期 -->
<view class="date_con">
<scroll-view scroll-y="true" class="date_box">
<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>
<!-- 時(shí)間 -->
<scroll-view scroll-y="true" class="time_box">
<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 { formatDate, toFixed, formatMinute } from '@/public/utils/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.$refs.datePickerPop.open();
} else {
this.$refs.datePickerPop.close();
}
}
},
data() {
// 生成取件日期
const recentDayNum = 5;
this.toFixed = toFixed;
return {
currentDate: formatDate(new Date(), '/'),
selectedTime: '',
selectedDate: {},
recentDateList: this.setRecentData(recentDayNum),
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: {
// 有效日期
effectRecentDate() {
const effectAppointmentTime = this.effectAppointmentTime;
// 當(dāng)日取件時(shí)間全部失效
if (effectAppointmentTime.length == 0) {
this.recentDateList.splice(0, 1);
this.selectedDate = this.recentDateList[0];
console.log('this.selectedDate: ', this.selectedDate);
return this.recentDateList;
} else {
return this.recentDateList;
}
},
// 有效取件時(shí)間
effectAppointmentTime() {
const appointment = this.appointment;
// 未來(lái)日期返回全部
if (this.selectedDate.date_en != this.currentDate) {
return appointment;
}
let list = appointment.filter((item) => this.checkRemainingMinute(item) != 1);
// 當(dāng)日只返回有效時(shí)間
if (list.length > 0) {
list.unshift('立即上門');
}
return list;
}
},
methods: {
handleClose() {
this.$emit('update:visible', false);
},
// 生成時(shí)間選擇器 最近n天的時(shí)間
setRecentData(n) {
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 ? '明天' : formatTime.week
});
}
this.selectedDate = list[0];
return list;
},
// 時(shí)間處理函數(shù)
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]
};
},
// 時(shí)間選擇器修改函數(shù)
timeChange(date, type) {
const dateList = this.recentDateList;
if (type === 'date') {
// 選擇日期
this.selectedDate = date;
this.selectedTime = '';
} else {
// 選擇時(shí)間
this.selectedTime = date;
if (this.selectedDate.date_zh == '') {
this.selectedDate = dateList[0];
}
this.handleClose();
this.$emit('selectTime', this.selectedDate, this.selectedTime);
}
},
/**
* @return {Number} 1:已過(guò)期 , 2:即將過(guò)期 , 3:未過(guò)期
*/
checkRemainingMinute(time) {
console.log('time: ', time);
if (!time) return;
const outTime = time.toString().split('-')[1];
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 : 已過(guò)期 --> 1
// minutes <= 90 : 即將過(guò)期 --> 2
// minutes > 0 : 未過(guò)期 --> 3
return minutes <= 0 ? 1 : minutes <= 90 ? 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: 100%;
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: 100%;
.disabled {
color: #ccc;
&::after {
content: '已過(guò)期';
margin-left: 130rpx;
}
}
.outTime {
color: #ccc;
&::after {
content: '即將過(guò)期';
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>
TODO:
- 時(shí)間區(qū)域打開顯示對(duì)應(yīng)選中時(shí)間位置
- 右側(cè)時(shí)間列表改后臺(tái)返回
以上就是uni-popup手?jǐn)]了一個(gè)菜鳥上門取件時(shí)間選擇器的詳細(xì)內(nèi)容,更多關(guān)于uni popup取件時(shí)間選擇器的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
vue解決使用webpack打包后keep-alive不生效的方法
今天小編就為大家分享一篇vue解決使用webpack打包后keep-alive不生效的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-09-09
Vue2.0利用vue-resource上傳文件到七牛的實(shí)例代碼
本篇文章主要介紹了Vue2.0利用vue-resource上傳文件到七牛的實(shí)例代碼,具有一定的參考價(jià)值,有興趣的可以了解一下2017-07-07
基于vue2.0實(shí)現(xiàn)的級(jí)聯(lián)選擇器
這篇文章主要介紹了基于vue2.0實(shí)現(xiàn)的級(jí)聯(lián)選擇器,基于Vue的級(jí)聯(lián)選擇器,可以單項(xiàng),二級(jí), 三級(jí)級(jí)聯(lián),多級(jí)級(jí)聯(lián),有興趣可以了解一下2017-06-06
element ui里dialog關(guān)閉后清除驗(yàn)證條件方法
下面小編就為大家分享一篇element ui里dialog關(guān)閉后清除驗(yàn)證條件方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-02-02
手把手教你搭建一個(gè)vue項(xiàng)目的完整步驟
身為入行未深的小白前端,不斷的學(xué)習(xí)是我們不可丟失的習(xí)慣,前端流行的框架也是層出不窮,vue在眾多框架中脫穎而出,下面這篇文章主要給大家介紹了關(guān)于搭建一個(gè)vue項(xiàng)目的完整步驟,需要的朋友可以參考下2022-07-07
vue awesome swiper異步加載數(shù)據(jù)出現(xiàn)的bug問題
這篇文章主要介紹了vue awesome swiper異步加載數(shù)據(jù)出現(xiàn)的bug問題,本文給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2018-07-07
解決vue項(xiàng)目 build之后資源文件找不到的問題
這篇文章主要介紹了解決vue項(xiàng)目 build之后資源文件找不到的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-09-09

