前言:
近期在做一个移动端项目,遇到一个自定义选择时间控件的需求;具体要实现的交互逻辑如下:
1. 预约下单的时候必须大于当前系统时间30分后,选择时间按照24小时制,分钟按照每隔15分钟进行显示(例如:15、30、45、00);
2. 如当前时间为14点30分,此时预约时间只能大于15点之后的时间用车(包含15点整);
3. 如当前系统时间为14点31分,此时预约时间只能大于15点整之后的时候用车(不包含15点整);
4.如选择了预约时间,但未提交,此时预约时间无需刷新,如再去选择预约时间则需要按照规则进行过滤可选择的时间,未去选择调整预约时间;
5.预约天数只能预约未来6天的时间;
效果图展示如下:
具体的实现代码如下:
/*
* @Description: 预约时间控件
* @Author: quan.wang
* @Date: 2022-11-25 10:53:03
* @LastEditors: quan.wang
* @LastEditTime: 2022-12-02 19:17:22
*/
/* eslint-disable no-param-reassign */
import React, { useEffect, useState } from 'react'
import { Picker } from 'antd-mobile'
import _ from 'lodash'
import './index.less'
export const everyMonthForDaysMap = {
1: 31,
2: 28,
3: 31,
4: 30,
5: 31,
6: 30,
7: 31,
8: 31,
9: 30,
10: 31,
11: 30,
12: 31,
}
const hoursEnum = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23]
const minutesEnum = [
{ label: '00分', value: '00' },
{ label: '15分', value: '15' },
{ label: '30分', value: '30' },
{ label: '45分', value: '45' },
]
// 计算小时
const calcHours = currentHour => {
return hoursEnum
.map(hour => {
if (hour >= currentHour) {
return {
label: `${hour}点`,
value: hour,
}
}
return ''
})
.filter(Boolean)
}
// 计算月、日
const calcMonthDay = (month, day) => {
const maxDay = everyMonthForDaysMap[month]
if (day > maxDay) {
return `${month + 1}月${day - maxDay}日`
}
return `${month}月${day}日`
}
// 计算日期(年、月、日)
const calcDate = (year, month, day) => {
if (year % 4 === 0) {
// 闰年,2月29天
everyMonthForDaysMap[2] = 29
}
const maxDay = everyMonthForDaysMap[month]
if (day > maxDay) {
return `${year}-${month + 1}-${day - maxDay}`
}
return `${year}-${month}-${day}`
}
const AppointTime = () => {
const newDate = new Date()
const currentDay = newDate.getDate()
const currentHour = newDate.getHours()
const currentMinute = newDate.getMinutes()
const currentYear = newDate.getFullYear()
const currentMonth = newDate.getMonth() + 1
const [visible, setVisible] = useState(false)
const [selectInfo, setSelectInfo] = useState('') // 由于Picker组件onSelect API钩子函数只能返回value, 所以需要将显示label和传给后端的时间格式拼接成一个字符串(为什么不写成对象格式,是因为对象格式的话,react无法作唯一性判断)
const [initialStartHour, setInitialStartHour] = useState(0) // 初始化小时数
const [data, setData] = useState([])
// 计算初始化开始小时
const calcInitialStartHour = dates => {
// 当minute超过15分钟时,16 + 30 ~ 60,所以初始化小时需要加1,并且还要考虑是否是23小时,如果是:当日不能预约,只能预约明天的
if (currentHour < 23) {
setInitialStartHour(currentHour + 1)
} else {
dates.shift()
setInitialStartHour(currentHour)
}
return dates
}
// 初始化获取范围日期
const getScopeDate = () => {
let dates = []
// 只能预约选择一个星期范围内时间
for (let i = 0; i < 7; i += 1) {
switch (i) {
case 0:
dates.push({
label: '今天',
value: `${currentYear}-${currentMonth}-${currentDay},今天`,
})
break
case 1:
dates.push({
label: '明天',
value: `${calcDate(currentYear, currentMonth, currentDay + i)},明天`,
})
break
case 2:
dates.push({
label: '后天',
value: `${calcDate(currentYear, currentMonth, currentDay + i)},后天`,
})
break
default:
dates.push({
label: calcMonthDay(currentMonth, currentDay + i),
value: `${calcDate(currentYear, currentMonth, currentDay + i)},${calcMonthDay(
currentMonth,
currentDay + i
)}`,
})
break
}
}
// 根据当前分钟数,按照30分钟一个间隔,来推算初始化可以选择的分钟集合
let minutes = []
if (currentMinute === 0) {
minutes = [
{ label: '30分', value: '30' },
{ label: '45分', value: '45' },
]
setInitialStartHour(currentHour)
} else if (currentMinute > 0 && currentMinute <= 15) {
minutes = [{ label: '45分', value: '45' }]
setInitialStartHour(currentHour)
} else if (currentMinute > 15 && currentMinute <= 30) {
minutes = minutesEnum
dates = calcInitialStartHour(dates)
} else if (currentMinute > 30 && currentMinute <= 45) {
minutes = [
{ label: '15分', value: '15' },
{ label: '30分', value: '30' },
{ label: '45分', value: '45' },
]
dates = calcInitialStartHour(dates)
} else if (currentMinute > 45) {
minutes = [
{ label: '30分', value: '30' },
{ label: '45分', value: '45' },
]
dates = calcInitialStartHour(dates)
}
setData([dates, calcHours(currentMinute <= 15 ? currentHour : currentHour + 1), minutes])
}
useEffect(() => {
getScopeDate()
}, [])
const isToday = selectDate => {
const [selectYear, selectMonth, selectDay] = selectDate.split('-')
return (
parseInt(selectYear, 10) === currentYear &&
parseInt(selectMonth, 10) === currentMonth &&
parseInt(selectDay, 10) === currentDay
)
}
const isinitialStartHour = selectHour => {
return initialStartHour === parseInt(selectHour, 10)
}
// 选择的日期是:当天并且是初始化小时的一些日期格式判断
const renderFinalDateByCurrentMinuteInInitialHour = () => {
let minutes = []
let hour = currentHour
if (currentMinute === 0) {
minutes = [
{ label: '30分', value: '30' },
{ label: '45分', value: '45' },
]
setData(prev => {
prev[1] = calcHours(currentHour)
prev[2] = minutes
return _.cloneDeep(prev)
})
} else if (currentMinute > 0 && currentMinute <= 15) {
minutes = [{ label: '45分', value: '45' }]
setData(prev => {
prev[1] = calcHours(currentHour)
prev[2] = minutes
return _.cloneDeep(prev)
})
} else if (currentMinute > 15 && currentMinute <= 30) {
hour = currentHour + 1
minutes = minutesEnum
setData(prev => {
prev[1] = calcHours(hour)
prev[2] = minutes
return _.cloneDeep(prev)
})
} else if (currentMinute > 30 && currentMinute <= 45) {
hour = currentHour + 1
minutes = [
{ label: '15分', value: '15' },
{ label: '30分', value: '30' },
{ label: '45分', value: '45' },
]
setData(prev => {
prev[1] = calcHours(initialStartHour)
prev[2] = minutes
return _.cloneDeep(prev)
})
} else if (currentMinute > 45) {
hour = currentHour + 1
minutes = [
{ label: '30分', value: '30' },
{ label: '45分', value: '45' },
]
setData(prev => {
prev[1] = calcHours(initialStartHour)
prev[2] = minutes
return _.cloneDeep(prev)
})
}
}
// 选择的日期是:当天并且是后面小时的日期格式化判断
const renderFinalDateByCurrentMinuteInNextHour = () => {
setData(prev => {
prev[1] = calcHours(initialStartHour)
// 分钟集合可以取全量的数据
prev[2] = minutesEnum
return _.cloneDeep(prev)
})
}
// 选择的日期是:未来天的日期格式化判断
const renderFinalDateInNextDay = () => {
setData(prev => {
prev[1] = calcHours(0)
prev[2] = minutesEnum
return _.cloneDeep(prev)
})
}
return (
<>
<p onClick={() => setVisible(true)}>{selectInfo ? selectInfo.label : '请选择用车时间'}</p>
<Picker
title="请选择时间"
onSelect={val => {
const [selectDate, selectHour] = val
// 转化成时间格式判断
const newSelectDate = selectDate.split(',')[0]
if (isToday(newSelectDate)) {
// 选择的小时是否是初始化设置的小时
if (isinitialStartHour(selectHour)) {
renderFinalDateByCurrentMinuteInInitialHour()
} else {
renderFinalDateByCurrentMinuteInNextHour()
}
} else {
renderFinalDateInNextDay()
}
}}
columns={data}
visible={visible}
onClose={() => {
setVisible(false)
}}
onConfirm={val => {
const [date, hour, minute] = val
const [formatDate, label] = date.split(',')
setSelectInfo({
label: `${label}${hour}点${minute}分`,
value: `${formatDate} ${hour}:${minute}`,
})
}}
/>
</>
)
}
export default AppointTime