星期-时间范围选择器 滑动选择时间 最小粒度 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><tdv-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:00afternoon: { start: 26, end: 35 }, // 13:00-18:00workdays: { 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><ZtWeekTimeRangev-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>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/469667.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

光流法与直接法在SLAM中的应用

本文总结视觉SLAM中常用的光流法与直接法 1、Lucas-Kanade光流法 相机所拍摄到的图像随相机视角的变化而变化&#xff0c;这种变化也可以理解为图像中像素的反向移动。“光流”&#xff08;Optical Flow&#xff09;是指通过分析连续图像帧来估计场景中像素或特征点的运动的技…

SSE (Server-Sent Events) 服务器实时推送详解

Server-Sent Events 一、什么是 SSE ?二、SSE 的工作原理三、SSE 的基本配置1.HTTP 请求和响应头设置2.SSE 字段介绍3.SSE 事件数据流示例 四、SseEmitter 的基本配置1.SseEmitter 介绍及用法2.使用 SseEmitter 示例11)编写核心 SSE Client2)编写 Controller3)前端接收与处理 …

AI大模型:重塑软件开发流程的优势、挑战及应对策略

随着人工智能技术的飞速发展&#xff0c;AI大模型正在深刻影响着软件开发的各个环节。本文将详细分析AI在软件开发流程中带来的优势&#xff0c;面临的挑战&#xff0c;以及开发者的应对策略。 一、AI在软件开发流程中的优势 提高开发效率 AI大模型能够自动生成高质量的代码…

《重学Java设计模式》之 原型模式

原型模式主要解决的问题就是创建重复对象&#xff0c;而这部分对象内容本身比较复杂&#xff0c;生成过程可能从库或者RPC接口中获取数据的耗时较长&#xff0c;因此采用克隆的方式节省时间。 案例&#xff1a;上机考试抽题&#xff0c;要求打乱题目、答案数据 工厂结构 选择题…

Java项目实战II基于Spring Boot的药店管理系统的设计与实现(开发文档+数据库+源码)

目录 一、前言 二、技术介绍 三、系统实现 四、文档参考 五、核心代码 六、源码获取 全栈码农以及毕业设计实战开发&#xff0c;CSDN平台Java领域新星创作者&#xff0c;专注于大学生项目实战开发、讲解和毕业答疑辅导。 一、前言 随着医疗行业的快速发展和人们对健康需…

html+js+css实现拖拽式便签留言

前些日子在网上冲浪时&#xff0c;看到一个便签式留言墙&#xff0c;让人耳目一新。心想这个看着不错&#xff0c;额想要。于是便开始搜寻是否有相应开源插件&#xff0c;想将其引入自己的博客中。但是搜寻了一圈&#xff0c;都没有符合预期的,要么功能不符合。有的功能符合&am…

模型压缩相关技术概念澄清(量化/剪枝/知识蒸馏)

1.模型压缩背景 随着深度学习技术的不断发展&#xff0c;模型的规模和复杂度也随之增加。大型模型往往具有更高的精度和更强的泛化能力&#xff0c;但在实际应用中&#xff0c;模型的大小却成为了一个制约因素。模型体积过大会导致存储、传输和推理速度等方面的瓶颈&#xff0…

Linux入门:环境变量与进程地址空间

一. 环境变量 1. 概念 1️⃣基本概念&#xff1a; 环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数 如&#xff1a;我们在编写C/C代码的时候&#xff0c;在链接的时候&#xff0c;从来不知道我们的所链接的动态静态库在哪里&#x…

Mysql前言

文章目录 Mysql 数据库简介SQL 基础语法什么是 SQL语句SQL 的作用SQL 语句的分类SQL 通用语法查询状态 &#x1f3e1;作者主页&#xff1a;点击&#xff01; &#x1f916;Mysql专栏&#xff1a;点击&#xff01; ⏰️创作时间&#xff1a;2024年11月12日18点20分 SQL是数据库…

VCSVerdi:KDB文件的生成和导入

相关阅读 VCShttps://blog.csdn.net/weixin_45791458/category_12828763.html Verdihttps://blog.csdn.net/weixin_45791458/category_12829428.html?spm1001.2014.3001.5482 前言 在复杂的设计中&#xff0c;很难在HDL或测试平台级别&#xff08;如使用系统函数&#xff…

2024年【汽车修理工(高级)】考试试卷及汽车修理工(高级)证考试

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 汽车修理工&#xff08;高级&#xff09;考试试卷是安全生产模拟考试一点通总题库中生成的一套汽车修理工&#xff08;高级&#xff09;证考试&#xff0c;安全生产模拟考试一点通上汽车修理工&#xff08;高级&#…

灵活就业,真的等同于失业吗?“三无人员”如何齐短板获贷款

现在灵活就业的人越来越多&#xff0c;目前有约2亿人选择灵活就业&#xff0c;今天咱们就来好好聊聊&#xff0c;灵活就业&#xff0c;它真的等同于失业吗&#xff1f; 咱们可以看看那些跑外卖的、做网约车司机的&#xff0c;虽然他们看起来在忙忙碌碌地工作&#xff0c;但细究…

python识别ocr 图片和pdf文件

#识别图片 pip3 install paddleocr pip3 install paddlepaddle#识别pdf pip3 install PyMuPDF 重点&#xff1a;路径不能有中文&#xff0c;不然pdf文件访问不了 from paddleocr import PaddleOCR from rest_framework.response import Response from rest_framework.views im…

由于找不到mfc120u.dll, 无法继续执行代码。重新安装程序可能解决引问题。

运行MFC程序报下面错误,无法到找运行库mfc120u.dll msvcr120.dll也找不到 下载C++运行库安装程序 mfc12对应2013运行库 运行库安装成功

介绍和安装及数据类型

1、介绍和安装 1.1、简介 ClickHouse是俄罗斯的Yandex于2016年开源的列式存储数据库&#xff08;DBMS&#xff09;&#xff0c;使用C语言编写&#xff0c;主要用于在线分析处理查询&#xff08;OLAP&#xff09;&#xff0c;能够使用SQL查询实时生成分析数据报告。 OLAP&…

【Pikachu】越权访问实战

所谓理想&#xff0c;只是同时拥有实力的人才能说的“现实”。所谓弱就是一种罪。 1.Over Permission概述 如果使用A用户的权限去操作B用户的数据&#xff0c;A的权限小于B的权限&#xff0c;如果能够成功操作&#xff0c;则称之为越权操作。 越权漏洞形成的原因是后台使用了…

KubeVirt入门介绍

KubeVirt入门介绍 KubeVirt 是一个开源项目&#xff0c;旨在通过 Kubernetes 管理虚拟机&#xff08;VM&#xff09;&#xff0c;使得 Kubernetes 不仅支持容器化工作负载&#xff0c;还支持虚拟机的部署和管理。这种双重支持的目标是提供一个统一的云原生平台&#xff0c;让开…

分布式----Ceph部署

目录 一、存储基础 1.1 单机存储设备 1.2 单机存储的问题 1.3 商业存储解决方案 1.4 分布式存储&#xff08;软件定义的存储 SDS&#xff09; 1.5 分布式存储的类型 二、Ceph 简介 三、Ceph 优势 四、Ceph 架构 五、Ceph 核心组件 #Pool中数据保存方式支持两种类型&…

智慧仓储物流可视化平台

通过图扑 HT 对仓储与物流流程进行动态可视化管理。实时跟踪库存状态和物流路径&#xff0c;优化资源配置与调度&#xff0c;提升运营效率及准确性&#xff0c;支持全流程决策优化及管理。

万字长文解读深度学习——卷积神经网络CNN

推荐阅读&#xff1a; 卷积神经网络&#xff08;CNN&#xff09;详细介绍及其原理详解 CNN笔记&#xff1a;通俗理解卷积神经网络 文章目录 &#x1f33a;深度学习面试八股汇总&#x1f33a;主要组件输入层卷积层 (Convolutional Layer)批归一化层&#xff08;Batch Normalizat…