• 记一次移动端封装自定义时间选择控件


    前言:

    近期在做一个移动端项目,遇到一个自定义选择时间控件的需求;具体要实现的交互逻辑如下:

    1. 预约下单的时候必须大于当前系统时间30分后,选择时间按照24小时制,分钟按照每隔15分钟进行显示(例如:15、30、45、00);
    2. 如当前时间为14点30分,此时预约时间只能大于15点之后的时间用车(包含15点整);

    3. 如当前系统时间为14点31分,此时预约时间只能大于15点整之后的时候用车(不包含15点整);

    4.如选择了预约时间,但未提交,此时预约时间无需刷新,如再去选择预约时间则需要按照规则进行过滤可选择的时间,未去选择调整预约时间;

    5.预约天数只能预约未来6天的时间;

    效果图展示如下:

     

    具体的实现代码如下:

    1. /*
    2. * @Description: 预约时间控件
    3. * @Author: quan.wang
    4. * @Date: 2022-11-25 10:53:03
    5. * @LastEditors: quan.wang
    6. * @LastEditTime: 2022-12-02 19:17:22
    7. */
    8. /* eslint-disable no-param-reassign */
    9. import React, { useEffect, useState } from 'react'
    10. import { Picker } from 'antd-mobile'
    11. import _ from 'lodash'
    12. import './index.less'
    13. export const everyMonthForDaysMap = {
    14. 1: 31,
    15. 2: 28,
    16. 3: 31,
    17. 4: 30,
    18. 5: 31,
    19. 6: 30,
    20. 7: 31,
    21. 8: 31,
    22. 9: 30,
    23. 10: 31,
    24. 11: 30,
    25. 12: 31,
    26. }
    27. 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]
    28. const minutesEnum = [
    29. { label: '00分', value: '00' },
    30. { label: '15分', value: '15' },
    31. { label: '30分', value: '30' },
    32. { label: '45分', value: '45' },
    33. ]
    34. // 计算小时
    35. const calcHours = currentHour => {
    36. return hoursEnum
    37. .map(hour => {
    38. if (hour >= currentHour) {
    39. return {
    40. label: `${hour}点`,
    41. value: hour,
    42. }
    43. }
    44. return ''
    45. })
    46. .filter(Boolean)
    47. }
    48. // 计算月、日
    49. const calcMonthDay = (month, day) => {
    50. const maxDay = everyMonthForDaysMap[month]
    51. if (day > maxDay) {
    52. return `${month + 1}${day - maxDay}日`
    53. }
    54. return `${month}${day}日`
    55. }
    56. // 计算日期(年、月、日)
    57. const calcDate = (year, month, day) => {
    58. if (year % 4 === 0) {
    59. // 闰年,2月29天
    60. everyMonthForDaysMap[2] = 29
    61. }
    62. const maxDay = everyMonthForDaysMap[month]
    63. if (day > maxDay) {
    64. return `${year}-${month + 1}-${day - maxDay}`
    65. }
    66. return `${year}-${month}-${day}`
    67. }
    68. const AppointTime = () => {
    69. const newDate = new Date()
    70. const currentDay = newDate.getDate()
    71. const currentHour = newDate.getHours()
    72. const currentMinute = newDate.getMinutes()
    73. const currentYear = newDate.getFullYear()
    74. const currentMonth = newDate.getMonth() + 1
    75. const [visible, setVisible] = useState(false)
    76. const [selectInfo, setSelectInfo] = useState('') // 由于Picker组件onSelect API钩子函数只能返回value, 所以需要将显示label和传给后端的时间格式拼接成一个字符串(为什么不写成对象格式,是因为对象格式的话,react无法作唯一性判断)
    77. const [initialStartHour, setInitialStartHour] = useState(0) // 初始化小时数
    78. const [data, setData] = useState([])
    79. // 计算初始化开始小时
    80. const calcInitialStartHour = dates => {
    81. // 当minute超过15分钟时,16 + 30 ~ 60,所以初始化小时需要加1,并且还要考虑是否是23小时,如果是:当日不能预约,只能预约明天的
    82. if (currentHour < 23) {
    83. setInitialStartHour(currentHour + 1)
    84. } else {
    85. dates.shift()
    86. setInitialStartHour(currentHour)
    87. }
    88. return dates
    89. }
    90. // 初始化获取范围日期
    91. const getScopeDate = () => {
    92. let dates = []
    93. // 只能预约选择一个星期范围内时间
    94. for (let i = 0; i < 7; i += 1) {
    95. switch (i) {
    96. case 0:
    97. dates.push({
    98. label: '今天',
    99. value: `${currentYear}-${currentMonth}-${currentDay},今天`,
    100. })
    101. break
    102. case 1:
    103. dates.push({
    104. label: '明天',
    105. value: `${calcDate(currentYear, currentMonth, currentDay + i)},明天`,
    106. })
    107. break
    108. case 2:
    109. dates.push({
    110. label: '后天',
    111. value: `${calcDate(currentYear, currentMonth, currentDay + i)},后天`,
    112. })
    113. break
    114. default:
    115. dates.push({
    116. label: calcMonthDay(currentMonth, currentDay + i),
    117. value: `${calcDate(currentYear, currentMonth, currentDay + i)},${calcMonthDay(
    118. currentMonth,
    119. currentDay + i
    120. )}`,
    121. })
    122. break
    123. }
    124. }
    125. // 根据当前分钟数,按照30分钟一个间隔,来推算初始化可以选择的分钟集合
    126. let minutes = []
    127. if (currentMinute === 0) {
    128. minutes = [
    129. { label: '30分', value: '30' },
    130. { label: '45分', value: '45' },
    131. ]
    132. setInitialStartHour(currentHour)
    133. } else if (currentMinute > 0 && currentMinute <= 15) {
    134. minutes = [{ label: '45分', value: '45' }]
    135. setInitialStartHour(currentHour)
    136. } else if (currentMinute > 15 && currentMinute <= 30) {
    137. minutes = minutesEnum
    138. dates = calcInitialStartHour(dates)
    139. } else if (currentMinute > 30 && currentMinute <= 45) {
    140. minutes = [
    141. { label: '15分', value: '15' },
    142. { label: '30分', value: '30' },
    143. { label: '45分', value: '45' },
    144. ]
    145. dates = calcInitialStartHour(dates)
    146. } else if (currentMinute > 45) {
    147. minutes = [
    148. { label: '30分', value: '30' },
    149. { label: '45分', value: '45' },
    150. ]
    151. dates = calcInitialStartHour(dates)
    152. }
    153. setData([dates, calcHours(currentMinute <= 15 ? currentHour : currentHour + 1), minutes])
    154. }
    155. useEffect(() => {
    156. getScopeDate()
    157. }, [])
    158. const isToday = selectDate => {
    159. const [selectYear, selectMonth, selectDay] = selectDate.split('-')
    160. return (
    161. parseInt(selectYear, 10) === currentYear &&
    162. parseInt(selectMonth, 10) === currentMonth &&
    163. parseInt(selectDay, 10) === currentDay
    164. )
    165. }
    166. const isinitialStartHour = selectHour => {
    167. return initialStartHour === parseInt(selectHour, 10)
    168. }
    169. // 选择的日期是:当天并且是初始化小时的一些日期格式判断
    170. const renderFinalDateByCurrentMinuteInInitialHour = () => {
    171. let minutes = []
    172. let hour = currentHour
    173. if (currentMinute === 0) {
    174. minutes = [
    175. { label: '30分', value: '30' },
    176. { label: '45分', value: '45' },
    177. ]
    178. setData(prev => {
    179. prev[1] = calcHours(currentHour)
    180. prev[2] = minutes
    181. return _.cloneDeep(prev)
    182. })
    183. } else if (currentMinute > 0 && currentMinute <= 15) {
    184. minutes = [{ label: '45分', value: '45' }]
    185. setData(prev => {
    186. prev[1] = calcHours(currentHour)
    187. prev[2] = minutes
    188. return _.cloneDeep(prev)
    189. })
    190. } else if (currentMinute > 15 && currentMinute <= 30) {
    191. hour = currentHour + 1
    192. minutes = minutesEnum
    193. setData(prev => {
    194. prev[1] = calcHours(hour)
    195. prev[2] = minutes
    196. return _.cloneDeep(prev)
    197. })
    198. } else if (currentMinute > 30 && currentMinute <= 45) {
    199. hour = currentHour + 1
    200. minutes = [
    201. { label: '15分', value: '15' },
    202. { label: '30分', value: '30' },
    203. { label: '45分', value: '45' },
    204. ]
    205. setData(prev => {
    206. prev[1] = calcHours(initialStartHour)
    207. prev[2] = minutes
    208. return _.cloneDeep(prev)
    209. })
    210. } else if (currentMinute > 45) {
    211. hour = currentHour + 1
    212. minutes = [
    213. { label: '30分', value: '30' },
    214. { label: '45分', value: '45' },
    215. ]
    216. setData(prev => {
    217. prev[1] = calcHours(initialStartHour)
    218. prev[2] = minutes
    219. return _.cloneDeep(prev)
    220. })
    221. }
    222. }
    223. // 选择的日期是:当天并且是后面小时的日期格式化判断
    224. const renderFinalDateByCurrentMinuteInNextHour = () => {
    225. setData(prev => {
    226. prev[1] = calcHours(initialStartHour)
    227. // 分钟集合可以取全量的数据
    228. prev[2] = minutesEnum
    229. return _.cloneDeep(prev)
    230. })
    231. }
    232. // 选择的日期是:未来天的日期格式化判断
    233. const renderFinalDateInNextDay = () => {
    234. setData(prev => {
    235. prev[1] = calcHours(0)
    236. prev[2] = minutesEnum
    237. return _.cloneDeep(prev)
    238. })
    239. }
    240. return (
    241. <>
    242. <p onClick={() => setVisible(true)}>{selectInfo ? selectInfo.label : '请选择用车时间'}p>
    243. <Picker
    244. title="请选择时间"
    245. onSelect={val => {
    246. const [selectDate, selectHour] = val
    247. // 转化成时间格式判断
    248. const newSelectDate = selectDate.split(',')[0]
    249. if (isToday(newSelectDate)) {
    250. // 选择的小时是否是初始化设置的小时
    251. if (isinitialStartHour(selectHour)) {
    252. renderFinalDateByCurrentMinuteInInitialHour()
    253. } else {
    254. renderFinalDateByCurrentMinuteInNextHour()
    255. }
    256. } else {
    257. renderFinalDateInNextDay()
    258. }
    259. }}
    260. columns={data}
    261. visible={visible}
    262. onClose={() => {
    263. setVisible(false)
    264. }}
    265. onConfirm={val => {
    266. const [date, hour, minute] = val
    267. const [formatDate, label] = date.split(',')
    268. setSelectInfo({
    269. label: `${label}${hour}点${minute}分`,
    270. value: `${formatDate} ${hour}:${minute}`,
    271. })
    272. }}
    273. />
    274. )
    275. }
    276. export default AppointTime

  • 相关阅读:
    【初识JavaSe语法笔记起章】——标识符、关键字、字面常量、数据类型、类型转换与提升、字符串类型
    APK加固技术的演变,APK加固技术和不足之处
    [Python入门教程]01 Python开发环境搭建
    对称二叉树
    【计算机网络】计算机网络复习总结 ----- 链路层
    正则表达式:字符(2)
    电脑垃圾太多?这几个清理电脑的软件来看看吗?
    【学生网页设计作品 】关于HTML公益主题网页设计——公益电信诈骗10页
    vue2.0 使用可选链操作符
    Java 并发详解<二>
  • 原文地址:https://blog.csdn.net/u014165391/article/details/128153554