目录
一、组件的功能
它展示了一个时间选择器,用户可以根据不同时间段的可预约性进行交互操作。
二、组件结构
1.HTML模板和内联样式
主要包括两个数组:时间槽状态数组timePicker和时间槽时间数组timeList;
首先,先用v-for遍历timePicker展示出每一个时间槽。
其次,使用v-if判断选择性的展示出时间槽对应的时间。
最后,使用elementUI组件库的文字提示tooltip,实现鼠标悬浮展示对应时间槽时间的效果。
代码如下:
<template>
<div>
<div class="tip">
<span>质量创新中心</span>
<span>建议时长:60min</span>
<span>参观上限:60min</span>
</div>
<div class="picker" @mousemove="onMouseMove" @mouseup="onMouseUp">
<el-tooltip placement="top" effect="light" v-for="(item, index) in timePicker" :key="index"
:show-after="showAfter()" :content="getTimeContent(index)">
<div class="picker-item" :class="getitemClass(item)" @mousedown="onDrag(index)">
<div class="time-item" v-if="index % 4 === 0">{{ timeList[index] }}</div>
<div class="time-item-last" v-if="index === 35">{{ timeList[index + 1] }}</div>
</div>
</el-tooltip>
</div>
</div>
</template>
2.Script脚本
声明变量:
时间槽状态数组:timePicker,其中0代表空闲,1代表不可预定,2代表正在关注;
时间槽时间数组:timeList;
拖动状态:isDragging;
起始索引和结束索引:dragStartIndex、dragEndIndex;
事件:
计算时间槽:每个事件槽都是15分钟,最后一个需要单独拎出来手动添加;
根据时间槽的状态选择不同的样式:getitemClass;
获取悬浮文本:getTimeContent;
核心重点:三个事件,包括鼠标下落onDrag、鼠标移动onMouseMove和鼠标释放onMouseUp
鼠标下落事件onDrag:如果事件槽的状态是0则变成2;如果时间槽的状态是2则变成0,然后获取起始索引并修改拖动状态为鼠标移动事件做准备。
存在一种特殊情况:预约时间只能是一段连续的时间不能是多段。因为假设用户选中了4个连续的时间槽,当用户再次点击的时候需要清空。所以这里声明了一个变量flag用来判断用户点击的是否是正在关注的状态,然后把后面状态是2的全部变成0。
鼠标移动onMouseMove事件:首先判断拖动状态是否为true并且起始索引不为空。
然后判断结束索引不能等于当前鼠标的索引(通过getIndexFromEvent事件获取当前鼠标滑动的索引),之所以有这个判断是因为如果相等,说明没有进行拖动,就不用执行里面的逻辑。
最后如果结束索引不为空,执行setTimePicker事件,首先判断是正在选择还是正在取消选择,然后对时间槽进行设置。
在setTimePicker事件中,首先获取起始索引和结束索引的大小(因为有从前往后拖和从后往前托两种情况,所以需要判断大小,方便后面的遍历)。
如果是正在选择的情况,首先先遍历数组把所有2的状态全部变成0,然后不能超过60分钟,因为如果maxIndex - minIndex > 3会弹出提示并变成4个时间槽(60分钟),最后结束的时候修改拖动状态并把起始索引和结束索引变成空。
鼠标释放事件onMouseUp:拖动状态改为false,起始索引和结束索引设置为空。
代码如下:
<script setup lang="ts">
import { reactive, ref } from 'vue';
import { ElMessage } from 'element-plus';
// 0代表空闲,1代表不可预定,2代表正在关注
const timePicker = reactive<number[]>([0, 0, 0, 0, 1, 1, 1, 1, 0, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
// 时间表数组
const timeList = reactive<string[]>([]);
// 拖动状态
const isDragging = ref(false);
// 存储拖动起始索引
const dragStartIndex = ref<number | null>(null);
const dragEndIndex = ref<number | null>(null);
// 计算时间槽
const startHour = 9;
const endHour = 18;
const interval = 15;
for (let hour = startHour; hour <= endHour; hour++) {
for (let minute = 0; minute < 60; minute += interval) {
// 单独添加最后一个时间槽
if (hour === endHour && minute === 0) {
const time = `${hour}:${minute < 10 ? '0' : ''}${minute}`;
timeList.push(time);
break;
} else if (hour < endHour) {
const time = `${hour}:${minute < 10 ? '0' : ''}${minute}`;
timeList.push(time);
}
}
}
// 时间槽状态选择不同的样式
const getitemClass = (item: number) => {
if (item === 0) {
return 'timeitem-free';
} else if (item === 1) {
return 'timeitem-disabled';
} else {
return 'timeitem-active';
}
}
// 延迟显示时间提示
const showAfter = () => {
return 150;
}
// 获取悬浮文本
const getTimeContent = (index: number) => {
if (timePicker[index] === 0) {
return timeList[index] + ' - ' + timeList[index + 1] + ' 可预约';
} else if (timePicker[index] === 1) {
return timeList[index] + ' - ' + timeList[index + 1] + ' 不可预定';
} else {
const minIndex = timePicker.indexOf(2);
const maxIndex = timePicker.lastIndexOf(2);
return timeList[minIndex] + ' - ' + timeList[maxIndex + 1] + ' 正在关注';
}
}
// 鼠标下落事件
const onDrag = (e: number) => {
if (timePicker[e] === 0) {
timePicker.forEach((item, index) => {
if (item == 2) {
timePicker[index] = 0;
}
});
timePicker[e] = 2;
} else if (timePicker[e] === 2) {
timePicker[e] = 0;
let flag = false;//判断是否出现正在关注的
for (let i = 0; i < timePicker.length; i++) {
if (timePicker[i] == 2) {
flag = true;
}
if (timePicker[i] != 2 && flag) {
for (let j = i + 1; j < timePicker.length; j++) {
if (timePicker[j] == 2) {
timePicker[j] = 0;
}
}
break;
}
}
} else {
ElMessage({
message: '不可预约',
type: 'error',
plain: true,
});
return;
}
dragStartIndex.value = e;
isDragging.value = true;
};
// 鼠标移动事件
const onMouseMove = (e: MouseEvent) => {
if (isDragging.value && dragStartIndex.value !== null) {
//判断鼠标是否拖到了另一格子
if (getIndexFromEvent(e) == null) {
isDragging.value = false;
dragStartIndex.value = null;
dragEndIndex.value = null;
return;
}
if (dragEndIndex.value != getIndexFromEvent(e)) {
dragEndIndex.value = getIndexFromEvent(e);
if (dragEndIndex.value !== null) {
// 这里可以添加更多逻辑,例如根据拖动方向更新状态
if (timePicker[dragStartIndex.value] === 2) {
setTimePicker(true, dragStartIndex.value, dragEndIndex.value);
} else if (timePicker[dragStartIndex.value] === 0) {
setTimePicker(false, dragStartIndex.value, dragEndIndex.value);
}
}
}
}
};
const errorMessage = (message: string) => {
ElMessage({
message,
type: 'error',
plain: true,
offset: 85,
});
}
// 检查所选时间段是否符合规则:连续且不超过5个时间槽
const checkSelectedTimeslot = (): boolean => {
let number = 0
for (let i = 0; i < timePicker.length; i++) {
if (timePicker[i] === 2) {
number++;
}
if (number >= 4) {
errorMessage('预约时长不能超过60分钟');
isDragging.value = false;
dragStartIndex.value = null;
dragEndIndex.value = null;
return false
}
}
return true;
};
// setTimePicker 函数中用 checkSelectedTimeslot 函数进行检查
function setTimePicker(select: boolean, startIndex: number, endIndex: number) {
const minIndex = Math.min(startIndex, endIndex);
const maxIndex = Math.max(startIndex, endIndex);
if (select) {
if (checkSelectedTimeslot()) {
for (let i = minIndex; i <= maxIndex; i++) {
if (timePicker[i] !== 1 && timePicker[i] !== 3) {
timePicker[i] = 2;
} else {
errorMessage('不可预约');
isDragging.value = false;
dragStartIndex.value = null;
dragEndIndex.value = null;
return;
}
}
}
} else {
for (let i = minIndex; i <= maxIndex; i++) {
if (timePicker[i] !== 1 && timePicker[i] !== 3) {
timePicker[i] = 0;
} else {
errorMessage('不可预约');
return;
}
}
}
}
// 鼠标释放事件
const onMouseUp = () => {
isDragging.value = false;
dragStartIndex.value = null;
dragEndIndex.value = null;
};
// 根据鼠标事件计算索引
const getIndexFromEvent = (e: MouseEvent): number | null => {
const target = e.target as HTMLElement;
const pickerItems = document.querySelectorAll('.picker-item');
for (let i = 0; i < pickerItems.length; i++) {
if (pickerItems[i].contains(target)) {
return i;
}
}
return null;
};
</script>
3.Style样式
代码如下:
<style lang="scss" scoped>
.tip {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
height: 30px;
color: #999;
font-size: 14px;
margin-top: 50px;
}
.picker {
display: flex;
width: 100%;
height: 25px;
background-color: #E4F8FF;
border: 1px solid #0066C4;
margin: 20px 0 50px 0;
.picker-item {
position: relative;
flex: 1;
height: 100%;
border-right: 1px solid #0066C4;
user-select: none;
&:last-child {
border-right: none;
}
}
.time-item {
position: absolute;
bottom: -100%;
left: -70%;
}
.time-item-last {
position: absolute;
bottom: -100%;
right: -70%;
}
}
.timeitem-free {
background-color: #E4F8FF;
}
.timeitem-disabled {
background-color: #C1C1C1;
}
.timeitem-active {
background-color: #FFA770;
}
</style>