自学内容网 自学内容网

星期-时间范围选择器 滑动选择时间 最小粒度 vue3

根据业务需要,实现了一个可选择时间范围的周视图。用户可以通过鼠标拖动来选择时间段,并且可以通过快速选择组件来快速选择特定的时间范围。

在这里插入图片描述

功能介绍

  1. 时间范围选择:用户可以通过鼠标拖动来选择时间段。
  2. 快速选择:提供快速选择组件,用户可以通过点击快速选择特定的时间范围,如上午、下午、工作日、周末等。
  3. 自定义样式:可以通过selectionColor 属性自定义选中区域的颜色。
  4. 数据绑定:通过 modelValue属性与父组件进行数据绑定,实时更新选择的时间范围。

属性说明

modelValue:绑定的时间范围数据,类型为数组。
selectionColor:选中区域的颜色,类型为字符串,默认为 ‘rgba(5, 146, 245, 0.6)’。
showQuickSelect:是否显示快速选择组件,类型为布尔值,默认为 true。

事件说明

update:modelValue:当选择的时间范围发生变化时触发,返回更新后的时间范围数据。

实现代码

index.vue

<template>
  <div class="zt-weektime">
    <div :class="{ 'zt-schedule-notransi': mode }" :style="[styleValue, { backgroundColor: selectionColor }]" class="zt-schedule"></div>
    <table class="zt-weektime-table">
      <thead class="zt-weektime-head">
        <tr>
          <td class="week-td" rowspan="8"></td>
          <td v-for="t in theadArr" :key="t" :colspan="2">{{ t }}:00</td>
        </tr>
        <!--        <tr>-->
        <!--          <td v-for="t in 48" :key="t" class="half-hour">-->
        <!--            {{ t % 2 === 0 ? "00" : "30" }}-->
        <!--          </td>-->
        <!--        </tr>-->
      </thead>
      <tbody class="zt-weektime-body">
        <tr v-for="t in weekData" :key="t.row">
          <td>{{ t.value }}</td>
          <td
            v-for="n in t.child"
            :key="`${n.row}-${n.col}`"
            :class="['weektime-atom-item', { selected: isSelected(n) }]"
            :data-time="n.col"
            :data-week="n.row"
            :style="{ '--selection-color': selectionColor }"
            @mousedown="cellDown(n)"
            @mouseenter="cellEnter(n)"
            @mouseup="cellUp(n)"
          ></td>
        </tr>
        <tr>
          <td class="zt-weektime-preview" colspan="49">
            <QuickSelect v-if="showQuickSelect" style="padding: 10px 0" @select="handleQuickSelect" />
            <!--            <div class="g-clearfix zt-weektime-con">-->
            <!--              <span class="g-pull-left">{{ hasSelection ? "已选择时间段" : "可拖动鼠标选择时间段" }}</span>-->
            <!--            </div>-->
            <!--            <div v-if="hasSelection" class="zt-weektime-time">-->
            <!--              <div v-for="(ranges, week) in formattedSelections" :key="week">-->
            <!--                <p v-if="ranges.length">-->
            <!--                  <span class="g-tip-text">{{ week }}:</span>-->
            <!--                  <span>{{ ranges.join("、") }}</span>-->
            <!--                </p>-->
            <!--              </div>-->
            <!--            </div>-->
          </td>
        </tr>
      </tbody>
    </table>
  </div>
</template>
<script setup>
import { computed, defineEmits, defineProps, onMounted, ref, watch } from "vue";
import QuickSelect from "./quickSelect.vue";

defineOptions({
  name: "ZtWeekTimeRange"
});

const props = defineProps({
  modelValue: {
    type: Array,
    required: true,
    default: () => []
  },
  selectionColor: {
    type: String,
    default: "rgba(5, 146, 245, 0.6)"
  },
  showQuickSelect: {
    type: Boolean,
    default: true
  }
});

const emit = defineEmits(["update:modelValue"]);

const weekData = ref([
  { row: 0, value: "周一", child: [] },
  { row: 1, value: "周二", child: [] },
  { row: 2, value: "周三", child: [] },
  { row: 3, value: "周四", child: [] },
  { row: 4, value: "周五", child: [] },
  { row: 5, value: "周六", child: [] },
  { row: 6, value: "周日", child: [] }
]);

// UI State
const width = ref(0);
const height = ref(0);
const left = ref(0);
const top = ref(0);
const mode = ref(0);
const startRow = ref(0);
const startCol = ref(0);
const endRow = ref(0);
const endCol = ref(0);
const isDragging = ref(false);
const theadArr = ref([]);

onMounted(() => {
  theadArr.value = Array.from({ length: 24 }, (_, i) => i);
  initializeGridData();
  syncSelectionFromValue();
});

watch(() => props.modelValue, syncSelectionFromValue, { deep: true });

function handleQuickSelect({ type, start, end, days }) {
  if (type === "morning" || type === "afternoon") {
    // 清除现有选择
    weekData.value.forEach((week) => {
      week.child.forEach((slot) => {
        if (slot.col >= start && slot.col <= end) {
          slot.selected = true;
        }
      });
    });
  } else if (type === "workdays" || type === "weekend") {
    days.forEach((dayIndex) => {
      weekData.value[dayIndex].child.forEach((slot) => {
        slot.selected = true;
      });
    });
  } else if (type === "all") {
    weekData.value.forEach((week) => {
      week.child.forEach((slot) => {
        slot.selected = true;
      });
    });
  } else if (type === "clean") {
    weekData.value.forEach((week) => {
      week.child.forEach((slot) => {
        slot.selected = false;
      });
    });
  }
  emitSelectionChange();
}

function formatTimeRange(start, end) {
  const formatTime = (slots) => {
    const hours = Math.floor(slots / 2);
    const minutes = (slots % 2) * 30;
    return `${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}`;
  };

  return `${formatTime(start)}-${formatTime(end)}`;
}

function initializeGridData() {
  weekData.value.forEach((week) => {
    week.child = Array.from({ length: 48 }, (_, i) => ({
      row: week.row,
      col: i,
      selected: false
    }));
  });
}

function syncSelectionFromValue() {
  weekData.value.forEach((week) => {
    week.child.forEach((slot) => {
      slot.selected = false;
    });
  });

  props.modelValue.forEach((selection) => {
    const { week, ranges } = selection;
    const weekIndex = weekData.value.findIndex((w) => w.value === week);
    if (weekIndex !== -1) {
      ranges.forEach((range) => {
        const [start, end] = range.split("-").map((time) => {
          const [hours, minutes] = time.split(":").map(Number);
          return hours * 2 + (minutes === 30 ? 1 : 0);
        });

        for (let i = start; i <= end; i++) {
          const slot = weekData.value[weekIndex].child[i];
          if (slot) slot.selected = true;
        }
      });
    }
  });
}

const styleValue = computed(() => ({
  width: `${width.value}px`,
  height: `${height.value}px`,
  left: `${left.value}px`,
  top: `${top.value}px`
}));

const hasSelection = computed(() => {
  return weekData.value.some((week) => week.child.some((slot) => slot.selected));
});

const formattedSelections = computed(() => {
  const selections = {};

  weekData.value.forEach((week) => {
    const ranges = [];
    let start = null;

    week.child.forEach((slot, index) => {
      if (slot.selected && start === null) {
        start = index;
      } else if (!slot.selected && start !== null) {
        ranges.push(formatTimeRange(start, index - 1));
        start = null;
      }
    });

    if (start !== null) {
      ranges.push(formatTimeRange(start, week.child.length - 1));
    }

    if (ranges.length) {
      selections[week.value] = ranges;
    }
  });

  return selections;
});

function isSelected(slot) {
  return slot.selected;
}

function cellDown(item) {
  isDragging.value = true;
  startRow.value = item.row;
  startCol.value = item.col;
  endRow.value = item.row;
  endCol.value = item.col;

  const ele = document.querySelector(`td[data-week='${item.row}'][data-time='${item.col}']`);
  if (ele) {
    width.value = ele.offsetWidth;
    height.value = ele.offsetHeight;
    left.value = ele.offsetLeft;
    top.value = ele.offsetTop;
  }
  mode.value = 1;
}

function cellEnter(item) {
  if (!isDragging.value) return;

  endRow.value = item.row;
  endCol.value = item.col;

  const ele = document.querySelector(`td[data-week='${item.row}'][data-time='${item.col}']`);
  if (!ele) return;

  const minRow = Math.min(startRow.value, endRow.value);
  const maxRow = Math.max(startRow.value, endRow.value);
  const minCol = Math.min(startCol.value, endCol.value);
  const maxCol = Math.max(startCol.value, endCol.value);

  const startEle = document.querySelector(`td[data-week='${minRow}'][data-time='${minCol}']`);
  if (startEle) {
    left.value = startEle.offsetLeft;
    top.value = startEle.offsetTop;
    width.value = (maxCol - minCol + 1) * ele.offsetWidth;
    height.value = (maxRow - minRow + 1) * ele.offsetHeight;
  }
}

function cellUp() {
  if (!isDragging.value) return;

  const minRow = Math.min(startRow.value, endRow.value);
  const maxRow = Math.max(startRow.value, endRow.value);
  const minCol = Math.min(startCol.value, endCol.value);
  const maxCol = Math.max(startCol.value, endCol.value);

  const isDeselecting = weekData.value[minRow].child[minCol].selected;

  weekData.value.forEach((week, weekIndex) => {
    if (weekIndex >= minRow && weekIndex <= maxRow) {
      week.child.forEach((slot, slotIndex) => {
        if (slotIndex >= minCol && slotIndex <= maxCol) {
          slot.selected = !isDeselecting;
        }
      });
    }
  });

  isDragging.value = false;
  width.value = 0;
  height.value = 0;
  mode.value = 0;

  emitSelectionChange();
}

function emitSelectionChange() {
  const selections = weekData.value
    .map((week) => {
      const ranges = [];
      let start = null;

      week.child.forEach((slot, index) => {
        if (slot.selected && start === null) {
          start = index;
        } else if (!slot.selected && start !== null) {
          ranges.push(formatTimeRange(start, index - 1));
          start = null;
        }
      });

      if (start !== null) {
        ranges.push(formatTimeRange(start, week.child.length - 1));
      }

      return {
        week: week.value,
        ranges
      };
    })
    .filter((week) => week.ranges.length > 0);

  emit("update:modelValue", selections);
}
</script>

<style scoped>
.zt-weektime {
  min-width: 600px;
  position: relative;
  display: inline-block;
}

.zt-schedule {
  position: absolute;
  width: 0;
  height: 0;
  pointer-events: none;
  transition: background-color 0.3s ease;
}

.zt-schedule-notransi {
  transition:
    width 0.12s cubizt-bezier(0.4, 0, 0.2, 1),
    height 0.12s cubizt-bezier(0.4, 0, 0.2, 1),
    top 0.12s cubizt-bezier(0.4, 0, 0.2, 1),
    left 0.12s cubizt-bezier(0.4, 0, 0.2, 1);
}

.zt-weektime-table {
  border-collapse: collapse;
  width: 100%;
}

.zt-weektime-table th,
.zt-weektime-table td {
  user-select: none;
  border: 1px solid #dee4f5;
  text-align: center;
  min-width: 12px;
  line-height: 1.8em;
  transition: background 0.2s ease;
}

.zt-weektime-table tr {
  height: 30px;
}

.zt-weektime-head {
  font-size: 12px;
}

.zt-weektime-head .week-td {
  min-width: 40px;
  width: 70px;
}

.half-hour {
  color: #666;
  font-size: 10px;
}

.zt-weektime-body {
  font-size: 12px;
}

.weektime-atom-item {
  user-select: unset;
  background-color: #f5f5f5;
  cursor: pointer;
  width: 20px;
  transition: background-color 0.3s ease;
}

.weektime-atom-item.selected {
  background-color: var(--selection-color, rgba(5, 146, 245, 0.6));
  animation: selectPulse 0.3s ease-out;
}

@keyframes selectPulse {
  0% {
    transform: scale(0.95);
    opacity: 0.7;
  }
  50% {
    transform: scale(1.02);
    opacity: 0.85;
  }
  100% {
    transform: scale(1);
    opacity: 1;
  }
}

.zt-weektime-preview {
  line-height: 2.4em;
  padding: 0 10px;
  font-size: 14px;
}

.zt-weektime-preview .zt-weektime-con {
  line-height: 46px;
  user-select: none;
}

.zt-weektime-preview .zt-weektime-time {
  text-align: left;
  line-height: 2.4em;
}

.zt-weektime-preview .zt-weektime-time p {
  max-width: 625px;
  line-height: 1.4em;
  word-break: break-all;
  margin-bottom: 8px;
}

.g-clearfix:after,
.g-clearfix:before {
  clear: both;
  content: " ";
  display: table;
}

.g-pull-left {
  float: left;
}

.g-tip-text {
  color: #999;
}
</style>

quickSelect.vue

<template>
  <div class="quick-select">
    <el-button-group>
      <el-button v-for="option in quickOptions" :key="option.key" size="small" @click="handleQuickSelect(option.key)">
        {{ option.label }}
      </el-button>
    </el-button-group>
    <el-button-group>
      <el-button size="small" @click="handleQuickSelect('all')"> 全选</el-button>
      <el-button size="small" @click="handleQuickSelect('clean')"> 清空</el-button>
    </el-button-group>
  </div>
</template>

<script setup>
const props = defineProps({
  show: {
    type: Boolean,
    default: true
  }
});

const emit = defineEmits(["select"]);

const quickOptions = [
  { key: "morning", label: "上午" },
  { key: "afternoon", label: "下午" },
  { key: "workdays", label: "工作日" },
  { key: "weekend", label: "周末" }
];

const timeRanges = {
  morning: { start: 16, end: 23 }, // 8:00-12:00
  afternoon: { start: 26, end: 35 }, // 13:00-18:00
  workdays: { days: [0, 1, 2, 3, 4] }, // 周一到周五
  weekend: { days: [5, 6] }, // 周六周日
  all: {}, // 全选
  clean: {} // 清空
};

function handleQuickSelect(type) {
  emit("select", { type, ...timeRanges[type] });
}
</script>

<style scoped>
.quick-select {
  display: flex;
  justify-content: space-between;
}
</style>

使用范例

效果:
在这里插入图片描述

实现代码:

<template>
  <div>
    <h1>时间段选择示例</h1>
    <div class="color-picker">
      <span>选择颜色:</span>
      <el-color-picker v-model="selectedColor" :predefine="predefineColors" show-alpha />
    </div>

    <ZtWeekTimeRange
      v-model="selectedTimeRanges"
      :selection-color="selectedColor"
      :show-quick-select="true"
      @update:modelValue="handleTimeRangeChange"
    />

    <div class="selected-ranges">
      <h3>选中的时间段:</h3>
      <pre style="height: 200px">{{ JSON.stringify(selectedTimeRanges, null, 2) }}</pre>
    </div>

    <div class="demo-controls">
      <button class="demo-button" @click="setDemoData">加载示例数据</button>
      <button class="demo-button" @click="clearSelection">清除选择</button>
    </div>
  </div>
</template>

<script setup>
import { ref } from "vue";

defineOptions({
  name: "星期时间范围选择器",
});

const selectedTimeRanges = ref([]);
const selectedColor = ref("rgba(5, 146, 245, 0.6)");

const predefineColors = [
  "rgba(5, 146, 245, 0.6)",
  "rgba(64, 158, 255, 0.6)",
  "rgba(103, 194, 58, 0.6)",
  "rgba(230, 162, 60, 0.6)",
  "rgba(245, 108, 108, 0.6)"
];

function handleTimeRangeChange(newRanges) {
  console.log("Time ranges updated:", newRanges);
}

function setDemoData() {
  selectedTimeRanges.value = [
    {
      week: "周一",
      ranges: ["09:00-12:00", "14:00-18:30"]
    },
    {
      week: "周三",
      ranges: ["10:30-16:00"]
    },
    {
      week: "周五",
      ranges: ["09:30-12:00", "13:30-17:30"]
    }
  ];
}

function clearSelection() {
  selectedTimeRanges.value = [];
}
</script>

<style>
.selected-ranges {
  padding: 15px;
  background: #f5f5f5;
  border-radius: 4px;
}

pre {
  background: #fff;
  padding: 10px;
  border-radius: 4px;
  overflow-x: auto;
}

.demo-controls {
  margin-top: 20px;
  display: flex;
  gap: 10px;
}

.demo-button {
  padding: 8px 16px;
  background-color: #0592f5;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  transition: background-color 0.2s;
}

.demo-button:hover {
  background-color: #0481d9;
}
</style>


原文地址:https://blog.csdn.net/weixin_44463883/article/details/143644464

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!