1. 项目概述
在移动端应用开发中,时间选择器是一个常见但又容易踩坑的组件。最近我在一个出行类项目中遇到了一个需求:要实现类似滴滴出行预约用车的时间选择弹窗。这个组件需要满足以下几个核心需求:
- 只能选择未来的时间(不能选择过去的时间)
- 日期范围限定为今天、明天、后天三天
- 小时和分钟需要根据当前时间动态调整可选范围
- 分钟以10分钟为间隔(如00、10、20...50)
- 当选择"今天"时,小时和分钟要从当前时间开始计算
经过技术选型,我决定基于Vant UI的Picker组件来实现这个功能。Vant UI是一套轻量、可靠的移动端Vue组件库,它的Picker组件提供了多列选择的基础能力,正好适合我们的需求场景。
2. 核心设计思路
2.1 数据结构设计
为了实现动态时间选择,我们需要维护几个关键数据:
javascript复制data() {
return {
showPicker: false, // 控制弹窗显示
selectedValues: ['今天', '00', '00'], // 当前选中的值 [日,时,分]
currentTime: new Date(), // 当前系统时间
displayValue: '', // 显示的时间文本
currentValue: [], // Picker绑定的值
};
}
2.2 计算属性设计
我们使用Vue的计算属性来动态生成各列选项:
dayOptions: 生成日期列选项(今天、明天、后天)hourOptions: 根据选择的日期生成小时选项minuteOptions: 根据选择的日期和小时生成分钟选项columns: 将上述选项组合成Picker需要的格式
2.3 动态联动逻辑
当用户改变某一列的选择时,需要动态更新后续列的选项。例如:
- 当从"今天"改为"明天"时,小时列应从00开始
- 当小时改变时,分钟列可能需要重新计算
3. 关键实现细节
3.1 日期选项生成
javascript复制dayOptions() {
const today = new Date();
let oneDay = this.tools.formate(today, 'MM月dd日');
let twoDay = this.tools.formate(today.getTime() + 24 * 60 * 60 * 1000, 'MM月dd日');
let threeDay = this.tools.formate(today.getTime() + 24 * 60 * 60 * 1000 * 2, 'MM月dd日');
let days = ['今天', '明天', '后天'];
return days.map(day => ({
text: day==='今天'?`${oneDay} 今天`:(day==='明天'?`${twoDay} 明天`:`${threeDay} 后天`),
value: day
}));
}
这里我们生成了包含格式化日期的文本,如"05月20日 今天",让用户更直观地理解日期。
3.2 小时选项生成
javascript复制hourOptions() {
const selectedDay = this.selectedValues[0];
const now = this.currentTime;
let startHour = 0;
let endHour = 23;
if (selectedDay === '今天') {
// 今天从当前小时开始
const currentMinute = now.getMinutes();
let startMinute = Math.ceil(currentMinute / 10) * 10;
startHour = startMinute===60?now.getHours()+1:now.getHours();
}
const hours = [];
for (let i = startHour; i <= endHour; i++) {
hours.push({
text: i.toString().padStart(2, '0') + '时',
value: i.toString().padStart(2, '0')
});
}
return hours;
}
这里的关键点是处理"今天"的情况:
- 如果当前时间是14:25,那么小时应从14开始
- 但如果当前时间是14:55,分钟向上取整后是60,这时小时应从15开始
3.3 分钟选项生成
javascript复制minuteOptions() {
const selectedDay = this.selectedValues[0];
const selectedHour = parseInt(this.selectedValues[1]);
const now = this.currentTime;
let startMinute = 0;
const endMinute = 50;
if (selectedDay === '今天') {
const currentHour = now.getHours();
if (selectedHour <= currentHour) {
// 如果是今天的当前小时及之前的小时,需从当前分钟开始
const currentMinute = now.getMinutes();
startMinute = Math.ceil(currentMinute / 10) * 10;
if (startMinute === 60) {
startMinute = 0;
}
}
}
const minutes = [];
for (let i = startMinute; i <= endMinute; i += 10) {
minutes.push({
text: i.toString().padStart(2, '0') + '分',
value: i.toString().padStart(2, '0')
});
}
return minutes;
}
分钟处理逻辑:
- 以10分钟为间隔(00、10、20...50)
- 如果是"今天"的当前小时,分钟从当前时间向上取整
- 处理60分钟的边界情况
3.4 动态联动实现
当用户改变选择时,通过onPickerChange方法更新后续列的选项:
javascript复制onPickerChange(picker, values, indexes) {
this.selectedValues = values.map((item, index) => {
if (typeof item === 'object') {
return item.value;
}
return item;
});
// 动态更新后两列
setTimeout(() => {
picker.setColumnValues(1, this.hourOptions);
picker.setColumnValues(2, this.minuteOptions);
this.adjustSelectedValues(picker);
}, 0);
}
这里使用setTimeout是为了确保DOM更新完成后再设置新值。
3.5 初始时间设置
组件初始化时,需要将选择器设置为当前时间:
javascript复制initializeSelection() {
const now = this.currentTime;
const currentHour = now.getHours().toString().padStart(2, '0');
const optionalMinute = (Math.ceil(now.getMinutes() / 10) * 10).toString().padStart(2, '0');
const currentMinute = now.getMinutes().toString().padStart(2, '0');
let startMinute = Math.ceil(optionalMinute / 10) * 10;
if(Number(currentHour)===23&&Number(currentMinute)>50){
// 23:50之后只能选明天
this.selectedValues = ['明天', '00', '00'];
}else{
if (startMinute === 60) {
this.selectedValues = ['今天', (Number(currentHour)+1).toString(), '00'];
}else{
this.selectedValues = ['今天', currentHour, currentMinute];
}
}
}
这里处理了几个边界情况:
- 23:50之后只能选择明天的时间
- 分钟为60时的特殊处理
4. 样式定制
为了让选择器更符合设计需求,我们做了以下样式调整:
scss复制/deep/ .van-picker {
padding: 0 16px;
background: transparent;
.van-picker-column.day-column{
width: 160px !important;
flex: none !important;
}
.van-picker-column__item {
font-size: 15px;
color: #666666;
}
.van-picker-column__item--selected {
font-size: 16px;
background: #f1eefb;
color: #333333;
}
}
/* 移除渐变遮罩 */
/deep/ .datetime-picker .van-picker__mask {
background: transparent !important;
}
关键样式点:
- 调整日期列的宽度
- 自定义选中项的样式
- 移除默认的渐变遮罩效果
5. 使用示例
在父组件中使用这个时间选择器:
html复制<appointment-datetimepicker
:isShow="isShowDateTimerPicker"
@handleDatetimePicker="handleDatetimePicker"
@sure="goAppointment">
</appointment-datetimepicker>
对应的JS逻辑:
javascript复制// 接收选中时间数据并关闭弹窗
goAppointment(dateVal){
this.appointmentTime = dateVal;
this.isShowDateTimerPicker = false;
},
// 更新时间弹窗数据
handleDatetimePicker(itemData){
this.isShowDateTimerPicker = false;
},
6. 常见问题与解决方案
6.1 选择器联动不流畅
问题现象:在快速滑动选择器时,联动效果有延迟或卡顿。
解决方案:
- 使用
setTimeout确保DOM更新完成后再设置新值 - 对频繁操作进行防抖处理
- 减少不必要的计算,缓存计算结果
6.2 边界时间处理
问题现象:在23:50之后初始化选择器时,分钟选项可能不正确。
解决方案:
javascript复制if(Number(currentHour)===23&&Number(currentMinute)>50){
this.selectedValues = ['明天', '00', '00'];
}
6.3 性能优化
优化建议:
- 对于不变的选项(如日期选项),可以在created钩子中计算并缓存
- 避免在计算属性中进行复杂计算
- 使用虚拟列表优化大量选项的渲染
7. 扩展思考
这个时间选择器组件还可以进一步优化和扩展:
- 支持更多日期:可以扩展为支持未来7天的选择
- 自定义时间间隔:允许配置分钟间隔(如5分钟、15分钟)
- 禁用特定时间段:比如禁用凌晨时段
- 国际化支持:适配不同地区的日期时间格式
在实际项目中,时间选择器虽然看起来简单,但要做好用户体验需要考虑很多细节。特别是在处理边界情况和动态联动时,需要仔细测试各种场景。我在实现这个组件时,特别关注了以下几点:
- 用户体验:确保选择过程流畅,避免出现无效选项
- 代码可维护性:将复杂逻辑拆分为小函数,增加注释
- 性能:避免不必要的计算和渲染
- 可扩展性:设计灵活的接口,方便后续扩展功能
这个组件的实现过程中,最复杂的部分就是处理各种边界情况的时间计算。特别是当用户在接近午夜时间操作时,需要确保逻辑的正确性。通过这个小项目,我深刻体会到,好的组件设计不仅在于实现功能,更在于处理各种边缘情况和异常场景。