一、需求:在预约列表中实现点击可选择预约的时间段,类似钉钉的交互。
二、功能介绍:以半小时为预约的时间段,交互上存在两种区别,1.当只考虑选中的情况,当选择两个时间中间还有空白时间,也应该被选中(比如第一个选择11:00~11:30,第二个选择为15:00-15:30,那么就需要把11:00-15:30所包含的全部选中)2.只考虑取消时,取消交互分为两种情况,前提是需要将目前选中的时间段一分为二,当取消的在上半部分时,需要将选择取消的时间之前全部取消(比如目前选择的是11:00-15:30,取消选择的是12:00-12:30,那么12:30之前选中的都应该取消),下半部分同理。
三、分析:需要初始化48个div,label对应时间段,需要将列表的48个div合并,每个时间段此时会有预约状态,当已预约时,选中状态,禁止修改并展示已预约。当已过期时,为未选中状态,禁止修改并展示已过期,未预约可以进行操作。已预约优先级最该展示。交互上方式通过ref绑定,根据index区分,操作dom实现选中状态
四、实际实现:对数据进行梳理,通过人为交互,考虑多种情况。此时不仅仅作为开发的角度,应作为使用着的角度,因为此时不关系后端,就纯前端处理数据和交互。
五、步骤分析:
4.1初始化48个ref绑定,初始化label并合并列表状态
4.2当点击时保存当前点击的index,需要根据比较点击的index,让区间的index选中或取消
4.3当为选中时,需要对保存的index数组排序,第一个就为开始时间的index,最后一个就为结束时间的index,遍历48个ref ,根据比较index让区域内的都选中
4.4当为取消选中时,需要判断当取消的是最后一个时,需要将保存index重置为空数组,当不为最后一个时,再根据保存的index数组长度,判断是奇数还是偶数,判断当前取消的在数组的上半部分还是下半部分,根据所点击的index,过滤掉取消的index,再通过遍历ref,让区域内的都取消选中
六、总结:对于这种没有数据支撑,纯前端来实现,局限性比较大,每次交互都需要前端来保存状态,实际的难度还是跟交互关系很大,虽然这个交互看起来很号实现,但实际操作起来还是很麻烦,遇到这种提供出思维导图比较方便,根据大模块依次向下分析
七:代码实现
import { useEffect, useState, useRef } from 'react'
import styles from './index.less'
import { history, useDispatch, useSelector } from 'umi'
import { List, ActionSheet, Checkbox } from 'antd-mobile'
const TimeSheet = (props) => {
const dispatch = useDispatch()
var checkRef = []
[...Array(47)].forEach((m, n) => {
checkRef[n] = useRef()
})
const [saveIndex, setSaveIndex] = useState([])
const { visibleSheet, eventChange, type } = props
const sheetData = useSelector((state) => state.joinApprove.detailData)
const templateTime = useSelector((state) => state.meetingTemplate.templateTime)
const [checkTime, setCheckTime] = useState([])
const initData = [
{ label: '00:00 ~ 00:30' },
{ label: '00:30 ~ 01:00' },
{ label: '01:00 ~ 01:30' },
{ label: '01:30 ~ 02:00' },
{ label: '02:00 ~ 02:30' },
{ label: '02:30 ~ 03:00' },
{ label: '03:00 ~ 03:30' },
{ label: '03:30 ~ 04:00' },
{ label: '04:00 ~ 04:30' },
{ label: '04:30 ~ 05:00' },
{ label: '05:00 ~ 05:30' },
{ label: '05:30 ~ 06:00' },
{ label: '06:00 ~ 06:30' },
{ label: '06:30 ~ 07:00' },
{ label: '07:00 ~ 07:30' },
{ label: '07:30 ~ 08:00' },
{ label: '08:00 ~ 08:30' },
{ label: '08:30 ~ 09:00' },
{ label: '09:00 ~ 09:30' },
{ label: '09:30 ~ 10:00' },
{ label: '10:00 ~ 10:30' },
{ label: '10:30 ~ 11:00' },
{ label: '11:00 ~ 11:30' },
{ label: '11:30 ~ 12:00' },
{ label: '12:00 ~ 12:30' },
{ label: '12:30 ~ 13:00' },
{ label: '13:00 ~ 13:30' },
{ label: '13:30 ~ 14:00' },
{ label: '14:00 ~ 14:30' },
{ label: '14:30 ~ 15:00' },
{ label: '15:00 ~ 15:30' },
{ label: '15:30 ~ 16:00' },
{ label: '16:00 ~ 16:30' },
{ label: '16:30 ~ 17:00' },
{ label: '17:00 ~ 17:30' },
{ label: '17:30 ~ 18:00' },
{ label: '18:00 ~ 18:30' },
{ label: '18:30 ~ 19:00' },
{ label: '19:00 ~ 19:30' },
{ label: '19:30 ~ 20:00' },
{ label: '20:00 ~ 20:30' },
{ label: '20:30 ~ 21:00' },
{ label: '21:00 ~ 21:30' },
{ label: '21:30 ~ 22:00' },
{ label: '22:00 ~ 22:30' },
{ label: '22:30 ~ 23:00' },
{ label: '23:00 ~ 23:30' },
{ label: '23:30 ~ 00:00' },
]
if (type === 'appoint') {
if (sheetData && JSON.stringify(sheetData) != '{}') {
initData.forEach((item, index) => {
item['type'] = sheetData.appointmentList[index].type
})
}
} else {
if (templateTime.length != 0) {
initData.forEach((item, index) => {
item['type'] = templateTime[index].type
})
}
}
useEffect(() => {
checkRef.forEach((m, n) => {
checkRef[n].current?.uncheck()
})
}, [props])
const listHeader = () => {
if (sheetData && JSON.stringify(sheetData) != '{}') {
return (
<div className={styles.topContent}>
<div className={styles.topAddress}>
<span>
{sheetData.officeLocation}
{sheetData.name}
</span>
</div>
<div className={styles.topMedia}>
{sheetData.equipment.split(',').map((item, index) => {
return (
<span key={index} style={{ marginRight: '6px' }}>
{item}
</span>
)
})}
</div>
<div className={styles.topRage}>
<div>
<img
style={{ width: '16px', height: '16px', marginBottom: '4px' }}
src={require('../../assets/appoint/person.png')}
></img>
<span style={{ marginLeft: '3px', marginRight: '8px' }}>{sheetData.capacity}</span>
</div>
<div>
<img
style={{ width: '16px', height: '16px', marginBottom: '4px' }}
src={require('../../assets/appoint/place.png')}
></img>
{sheetData.officeLocation}
</div>
</div>
</div>
)
}
}
// 遍历生成数组
const rangeOfNumbers = (startNum, endNum) => {
return startNum <= endNum ? [startNum].concat(rangeOfNumbers(startNum + 1, endNum)) : []
}
const checkChange = (val, item, index) => {
// console.log(val, 'sss', item, index)
// console.log(checkRef[index].current.check(),'checkRef')
let transData = JSON.parse(JSON.stringify(checkTime))
let transIndex = JSON.parse(JSON.stringify(saveIndex))
// 判断是选中还是取消选中
if (val) {
transIndex.push(index)
// console.log(transIndex, '打印打印')
// 当保存的数组有两项或者以上,是区间模式
if (transIndex.length > 0) {
transIndex.sort()
let minIndex = transIndex[0]
let maxIndex = transIndex[transIndex.length - 1]
// console.log(transIndex, 'transIndextransIndex')
// console.log(transIndex[0], 'kkk', transIndex[transIndex.length - 1])
// 通过遍历对 对应的ref选择
checkRef.forEach((m, n) => {
if (minIndex <= n && n <= maxIndex) {
checkRef[n].current.check()
}
})
}
// 保存当前的index区间
setSaveIndex(transIndex)
// 保存当前的label
transData.push(item.label)
setCheckTime(transData)
} else {
// console.log(transIndex, 'transIndex')
if (transIndex.length > 0) {
// 判断当元素为一个,取消时则为空数组
if (transIndex.length === 1 && checkTime.length === 1) {
setSaveIndex([])
} else {
// 通过遍历保存的index区域生成迭代数组
let middleIndex = rangeOfNumbers(transIndex[0], transIndex[transIndex.length - 1])
// console.log(middleIndex, 'middleIndex')
// 判断是奇数还是偶数
let evenNumber = middleIndex.length % 2
let minIndex
let maxIndex
let lastIndex
// 当为偶数时
if (evenNumber === 0) {
let middleLeft = middleIndex[middleIndex.length / 2 - 1]
let middleRight = middleIndex[middleIndex.length / 2]
if (index <= middleLeft) {
// 更新当前的index数组
setSaveIndex(middleIndex.filter((v) => v > index))
// 过滤掉的数组,进去下一次循环
lastIndex = middleIndex.filter((v) => v < index)
minIndex = lastIndex[0]
maxIndex = lastIndex[lastIndex.length - 1]
// console.log(lastIndex, 'lastIndex1')
}
if (index >= middleRight) {
setSaveIndex(middleIndex.filter((v) => v < index))
lastIndex = middleIndex.filter((v) => v > index)
minIndex = lastIndex[0]
maxIndex = lastIndex[lastIndex.length - 1]
// console.log(lastIndex, 'lastIndex2')
}
} else {
let middle = Math.trunc(middleIndex.length / 2)
if (index <= middleIndex[middle]) {
setSaveIndex(middleIndex.filter((v) => v > index))
lastIndex = middleIndex.filter((v) => v < index)
minIndex = lastIndex[0]
maxIndex = lastIndex[lastIndex.length - 1]
// console.log(lastIndex, 'lastIndex3')
}
if (index >= middleIndex[middle]) {
setSaveIndex(middleIndex.filter((v) => v < index))
lastIndex = middleIndex.filter((v) => v > index)
minIndex = lastIndex[0]
maxIndex = lastIndex[lastIndex.length - 1]
// console.log(lastIndex, 'lastIndex4')
}
}
// 通过遍历ref,将对应的ref设置为未选择
checkRef.forEach((m, n) => {
if (minIndex <= n && n <= maxIndex) {
checkRef[n].current.uncheck()
}
})
}
}
// 更新当前选择的时间
setCheckTime(transData.filter((v) => v != item.label))
}
}
const listData = () => {
return (
<div>
<List
header={listHeader()}
style={{ '--align-items': 'start', '--active-background-color': '#fff' }}
>
{
initData.map((item, index) => {
return (
<List.Item key={index}>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<Checkbox
ref={checkRef[index]}
disabled={item.type == 0 ? false : true}
defaultChecked={item.type == 1 ? true : false}
onChange={(val) => checkChange(val, item, index)}
>
{item.label}
</Checkbox>
<div>{item.type === 2 ? '已过期' : item.type === 1 ? '已预约' : ''}</div>
</div>
</List.Item>
)
})}
</List>
</div>
)
}
const actions = [
{
text: listData(),
key: 'copy',
},
{
text: (type === 'meetingChange' || type === 'againApprove') ? '完成' : '立即预定' ,
key: 'save',
disabled: saveIndex.length == 0 ? true : false,
onClick: () => {
eventChange(false)
// 将选择的时间存到全局状态
if (saveIndex.length > 0) {
let selectTime
if (saveIndex.length === 1) {
selectTime = [initData[saveIndex[0]]]
} else {
selectTime = [initData[saveIndex[0]], initData[saveIndex[saveIndex.length - 1]]]
}
dispatch({
type: 'joinApprove/saveSelectTime',
payload: selectTime,
})
}
},
},
]
return (
<div className={styles.timeSheet} id="timeSelect">
<ActionSheet
visible={visibleSheet}
actions={actions}
getContainer={() => document.getElementById('timeSelect')}
onClose={() => eventChange(false)}
/>
</div>
)
}
export default TimeSheet