最终效果图
一、 DragSelect插件说明
1.该插件为一款简单的javascript拖动选择选中插件DragSelect文档说明,类似 window 系统文件拖动选择效果,文档链接:DragSelectDoc: 简单的javascript拖动选择选中插件DragSelect文档说明,类似 window 系统文件拖动选择效果
2.该插件使用npm下载与实际不符,可能是插件作者并未上传npm库导致,故在本篇中,请将上述文档链接中的ds.min.js文件复制到本地,并对外暴露DragSelect即可。
修改本地ds.min.js文件重命名为dragSelect.js并在源码中暴露相关方法。
使用时通过import 引用即可
import DragSelect from '@/utils/dragSelect';
二、技术梳理
1.确认盒子数量
以06:00 - 22:30,每半个小时为一个单元为例,默认需要33个小盒子,若当前有预约数据,以10:00 - 11:30为例,则需要将对应时间段的三个小盒子替换为一个长度为三倍小盒子的长盒子。
2.补充所需数据
后台只会返回当前会议室的预约数据,所以我们需要动态补足其余时间段的数据,以半个小时为单位添加timeFrom、timeTo、status等相关所需数据。
3.响应式开发,兼容1280 - 1920屏幕分辨率
盒子的数量是固定的,屏幕的宽度确是不确定的,所以需要实时获取屏幕宽度或父盒子宽度,获取屏幕宽度也要通过计算实际盒子宽度,故本篇推荐只实时获取父盒子宽度,来实时计算每个小盒子的宽度。
三、实现方案
1.填充数据
1.首先,初始化06:00-22:30的时间数据
// 初始化时间
const initTime = computed(() => {
const arr = []
for (let i = 6; i <= 22; i++) {
let h = i < 10 ? `0${i}` : i
arr.push(`${h}:00`, `${h}:30`)
}
return arr
})
这样就拿到了间隔为30分钟的时间数组
2.后台返回数据如下
后台只会返回预约数据,而我们需要06:00 - 22:30的所有时间段的数据,所以需要判断时间是否连续,如果不连续则手动添加时间数据
// 检查时间空隙
const checkTime = (time, bookingList) => {
return bookingList.some(item => time >= item.timeFrom && time < item.timeTo)
}
判断条件为存在时间间隙且时间不是22:30,手动添加开始时间、结束时间、 及是否超过当前时间
// 填充时间数据
const meetingTime = () => {
meetingDetail.value.map(meeting => {
initTime.value.map((item, index) => {
if (!checkTime(item, meeting.bookingList) && item !== '22:30') {
meeting.bookingList.push({
timeFrom: item,
timeTo: initTime.value[index + 1],
status: Date.parse(`${props.date} ${initTime.value[index + 1]}`) <
Date.parse(nowDate.value) ? -1 : 0, // 判断过期状态
})
}
})
})
// 按照时间顺序排序
meetingDetail.value.map(item => {
item.bookingList.sort((prev, next) => {
let pTime = parseInt(prev.timeFrom.split(':').join(''))
let nTime = parseInt(next.timeFrom.split(':').join(''))
return pTime - nTime
})
})
}
最后拿到时间连贯的最终数据
2.使用DragSelect绑定DOM
1.绑定dom
area属性通过id绑定最外层的大盒子,代表可选中区域;selectables通过类名绑定,代表可选择的元素节点,这里代表每个小盒子(每30分钟时间段)
因为在执行new DragSelect时要首先保证DOM节点是存在的,所以放到onMounted里执行。
onMounted(() => {
// 绑定dom
new DragSelect({
selectables: document.getElementsByClassName('li'), // 可被选中的节点
area: document.getElementById('main'), // 允许拖动的区域
multiSelectMode: false, // 选中新的区域时清空旧的元素 false为清空
// 选中
onElementSelect: el => {},
// 取消选中
onElementUnselect: el => {},
// 用户释放鼠标后触发,返回所有被选定的元素节点
callback: ele => {}
});
3.渲染小盒子(时间节点)
1.在拿到数据后,我们就要根据数据去动态渲染一个一个的小盒子,由于是响应式开发,所以需要先实时获取父盒子的宽度,这里推荐使用 ResizeObserver。同样放到onMounted里执行。
// 父盒子宽度
const boxWidth = ref(null)
// 获取父盒子宽度
const getBoxWidth = () => {
// 实时获取盒子宽度
const resizeObserver = new ResizeObserver(entries => {
// 减去相应margin
boxWidth.value = entries[0].contentRect.width - 46
});
// 指定要观察的dom
const box = document.getElementById('main');
if (box) resizeObserver.observe(box);
}
更详细的关于ResizeObserver说明可以查看下面链接,这里不做补充。
2.在实时获取父盒子宽度后,则可以动态渲染小盒子宽度。
在小盒子dom上绑定我们所需数据,以便进行dom处理。
<div class="ul" :meet="JSON.stringify({ roomId: item.id,roomName: item.roomName,location: item.cityName + '-' + item.floorName})">
<template v-for="meet in item.bookingList">
<el-popover v-if="[-1,1,2].includes(meet.status)" placement="top-start" :width="200" trigger="hover">
<template #reference>
<div class="li flex justify-center items-center" :timeFrom="meet.timeFrom" :timeTo="meet.timeTo" :status="meet.status" :isMine="meet.isMine">
<p class="line-clamp-1 text-sm">{{ meet.subject }}</p>
</div>
</template>
<div v-if="meet.status == 1 || meet.status == 2" class="w-full h-30">
<div class="w-full flex justify-between">
<div class="mb-4">
<el-icon>
<WarningFilled />
</el-icon>
<span class="h-5 font-bold text-base ml-1" style="color: black;">{{ meet.isMine == 1 ? '我的预约' : '已被预约' }}</span>
</div>
<p v-if="meet.isMine == '1' && meet.status == '1'" class="text-xsblue-level_2 text-xs mt-1 cursor-pointer" @click="showDialog('2',meet)">修改会议</p>
</div>
<div class="w-40 mx-auto">
<p>时间:{{ meet.timeFrom }} - {{ meet.timeTo }}</p>
<p>姓名:{{meet.bookingUserName }}</p>
<p>部门:{{ meet.deptName }}</p>
<p>手机:{{ meet.phone }}</p>
<el-tooltip class="box-item" effect="dark" :content="meet.subject" placement="top-start">
<p class="line-clamp-1">主题:{{ meet.subject }}</p>
</el-tooltip>
</div>
</div>
<div v-if="meet.status == -1" class="w-full h-5">
<div class="w-full mb-4">
<el-icon>
<WarningFilled />
</el-icon>
<span class="h-5 font-bold text-base ml-1" style="color: black;">已超时</span>
</div>
</div>
</el-popover>
<!-- 可预约 -->
<div class="li" v-else :timeFrom="meet.timeFrom" :timeTo="meet.timeTo" :status="meet.status" :isMine="meet.isMine">
</div>
</template>
</div>
通过watch属性监视父盒子宽度,触发渲染小盒子宽高
// 定义子盒子样式
const definitionChildBox = () => {
const childBox = document.querySelectorAll('.ul .li')
childBox.forEach(item => {
item.style.height = boxWidth.value / 33 + 'px'
let timeFrom = item.getAttribute('timeFrom')
let timeTo = item.getAttribute('timeTo')
let status = item.getAttribute('status')
let isMine = item.getAttribute('isMine')
// 判断时间差
let multiplier = (timeTo.split(':')[0] - timeFrom.split(':')[0]) * 2 + ((timeTo.split(':')[1] - timeFrom.split(':')[1]) / 30)
item.style.width = multiplier * boxWidth.value / 33 + 'px'
// 工作时间
if (timeFrom == '09:00' || timeFrom == '18:00') item.style.borderLeft = "1px solid rgb(232,110,48)"
// 依据情况显示不同高亮
// 重置
item.classList.remove('mine')
item.classList.remove('timeout')
item.classList.remove('appointment')
// 我的预约
if (isMine == 1) item.classList.add('mine')
else if (status == 1 || status == 2) item.classList.add('appointment')
else if (status == -1) item.classList.add('timeout')
})
}
// 渲染子盒子样式
watch(boxWidth, () => {
definitionChildBox()
//渲染时间线用 在父盒子宽度小于1270的情况下,时间线会特别拥挤,此时作相应处理
if (boxWidth.value > 1270) windowWidth.value = true
else windowWidth.value = false
})
4.处理时间线
在屏幕分辨率大于1640时展示效果如下:
在屏幕分辨率小于1640时展示效果如下:
在watch监视父盒子宽度时,同步判断是否展示完全时间线标识 。
// 渲染子盒子样式
watch(boxWidth, () => {
definitionChildBox()
//渲染时间线用 在父盒子宽度小于1270的情况下,时间线会特别拥挤,此时作相应处理
if (boxWidth.value > 1270) windowWidth.value = true
else windowWidth.value = false
})
通过windowWidth标识展示或隐藏 30分钟时间点
<div :style="{width: windowWidth ? '100%' : (boxWidth + 35 + 'px')}" class="h-10 text-sm text-gray-400 flex justify-around">
<div v-for="time in initTime">
<span v-if="windowWidth || time.split(':')[1] == '00'">{{ time }}</span>
</div>
</div>
5.用户选择校验
在用户在main区域内进行滑动选取时,整个区域都是可以捕捉的,所以要校验用户所选取区域是否合规,例如我们存在多个会议室时,用户可能同时选择两个会议室中的时间节点,亦或是用户选择两个会议室中的空白处,并没选择时间节点,又或是用户进行了跨区域操作,例如当前10:00 - 11:00已经有人预约了会议室,此时用户所选区域为09:00 - 12:00... 以上这些操作都是不被允许的,所以我们要在用户选择后进行校验。
// 用户释放鼠标后触发,返回所有被选定的元素节点
callback: ele => {
// 防止用户误操作
if (ele.length == 0) return false
// 校验用户操作
let flag = true
const parentFirstNode = ele[0].parentNode
const parentlastNode = ele[ele.length - 1].parentNode
// 获取所有子节点
const childNodeArr = Array.from(parentFirstNode.children)
childNodeArr.forEach(item => {
// 禁止用户跨区域操作
if (item.getAttribute('status') == 1 && ele[0].getAttribute('timeFrom') < item.getAttribute('timeFrom') && item.getAttribute('timeFrom') < ele[ele.length - 1].getAttribute('timeFrom')) {
ElMessage.error('禁止跨区域操作')
flag = false
}
})
// 中止
if (!flag) return false
// 判断用户选中节点是否属于同一会议室
ele.forEach(item => {
if (item.getAttribute('status') == 0 && parentFirstNode == parentlastNode) item.classList.add('checked')
else flag = false
})
// 中止
if (!flag) return false
checkedNode.value = ele
showDialog('1', JSON.parse(parentFirstNode.getAttribute('meet')), ele)
}
四、总结
以上就是基于Vue3 + DragSelect实现仿钉钉/企业微信会议室预约模块的二次开发方案,在本方案中,我们进行了填充数据、渲染dom、响应式开发等等...内容过多,所以有些地方并没有详细说明,若您有什么疑问或对我的内容进行指正,欢迎您在下方进行评论探讨。
希望本篇内容对您有所帮助。