使用Vue3和TypeScript封装一个时间选择器

目录


一、组件的功能

它展示了一个时间选择器,用户可以根据不同时间段的可预约性进行交互操作。

二、组件结构

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>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值