星期-时间范围选择器 滑动选择时间 最小粒度 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.mzph.cn/pingmian/60489.shtml

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

相关文章

云岚到家 秒杀抢购

目录 秒杀抢购业务特点 常用技术方案 抢券 抢券界面 进行抢券 我的优惠券列表 活动查询 系统设计 活动查询分析 活动查询界面显示了哪些数据&#xff1f; 面向高并发如何提高活动查询性能&#xff1f; 如何保证缓存一致性&#xff1f; 数据流 Redis数据结构设计 如…

JavaWeb常见注解

1.Controller 在 JavaWeb 开发中&#xff0c;Controller是 Spring 框架中的一个注解&#xff0c;主要用于定义控制器类&#xff08;Controller&#xff09;&#xff0c;是 Spring MVC 模式的核心组件之一。它表示该类是一个 Spring MVC 控制器&#xff0c;用来处理 HTTP 请求并…

光伏储能微电网协调控制器

安科瑞 Acrel-Tu1990 1. 产品介绍 ACCU-100微电网协调控制器是一款专为微电网、分布式发电和储能系统设计的智能协调控制设备。该装置能够兼容包括光伏系统、风力发电、储能系统以及充电桩等多种设备的接入。它通过全天候的数据采集与分析&#xff0c;实时监控光伏、风能、储…

【C++课程学习】:继承:默认成员函数

&#x1f381;个人主页&#xff1a;我们的五年 &#x1f50d;系列专栏&#xff1a;C课程学习 &#x1f389;欢迎大家点赞&#x1f44d;评论&#x1f4dd;收藏⭐文章 目录 构造函数 &#x1f369;默认构造函数&#xff08;这里指的是编译器生成的构造函数&#xff09;&#…

泷羽sec学习打卡-Linux基础2

声明 学习视频来自B站UP主 泷羽sec,如涉及侵权马上删除文章 笔记的只是方便各位师傅学习知识,以下网站只涉及学习内容,其他的都与本人无关,切莫逾越法律红线,否则后果自负 关于Linux的那些事儿-Base2 一、Linux-Base2linux有哪些目录呢&#xff1f;不同目录下有哪些具体的文件呢…

TCP拥塞控制

TCP拥塞控制&#xff08;Congestion Control&#xff09; 什么是拥塞控制&#xff1f; 拥塞控制(Congestion Control)主要针对整个网络中的数据传输速率进行调节&#xff0c;防止过多的数据注入网络中&#xff0c;这样可以使网络中的路由器或链路不致于过载&#xff0c;以避免…

Unity教程(十八)战斗系统 攻击逻辑

Unity开发2D类银河恶魔城游戏学习笔记 Unity教程&#xff08;零&#xff09;Unity和VS的使用相关内容 Unity教程&#xff08;一&#xff09;开始学习状态机 Unity教程&#xff08;二&#xff09;角色移动的实现 Unity教程&#xff08;三&#xff09;角色跳跃的实现 Unity教程&…

自动驾驶合集(更新中)

文章目录 车辆模型控制路径规划 车辆模型 车辆模型基础合集 控制 控制合集 路径规划 规划合集

网站架构知识之Ansible进阶(day022)

1.handler触发器 应用场景&#xff1a;一般用于分发配置文件时候&#xff0c;如果配置文件有变化&#xff0c;则重启服务&#xff0c;如果没有变化&#xff0c;则不重启服务 案列01&#xff1a;分发nfs配置文件&#xff0c;若文件发生改变则重启服务 2.when判断 用于给ans运…

整理5个优秀的微信小程序开源项目

​ 一、Bee GitHub: https://github.com/woniudiancang/bee Bee是一个餐饮点餐商城微信小程序&#xff0c;是针对餐饮行业推出的一套完整的餐饮解决方案&#xff0c;实现了用户在线点餐下单、外卖、叫号排队、支付、配送等功能&#xff0c;完美的使餐饮行业更高效便捷&#x…

微服务链路追踪skywalking安装

‌SkyWalking是一个开源的分布式追踪系统&#xff0c;主要用于监控和分析微服务架构下的应用性能。‌ 它提供了分布式追踪、服务网格遥测分析、度量聚合和可视化一体化解决方案&#xff0c;特别适用于微服务、云原生架构和基于容器的环境&#xff08;如Docker、K8s、Mesos&…

5G的发展演进

5G发展的驱动力 什么是5G [远程会议&#xff0c;2020年7月10日] 在来自世界各地的政府主管部门、电信制造及运营企业、研究机构约200多名会议代表和专家们的共同见证下&#xff0c;ITU-R WP 5D#35e远程会议宣布3GPP 5G技术&#xff08;含NB-IoT&#xff09;满足IMT-2020 5G技…

matlab建模入门指导

本文以水池中鸡蛋温度随时间的变化为切入点&#xff0c;对其进行数学建模并进行MATLAB求解&#xff0c;以更为通俗地进行数学建模问题入门指导。 一、问题简述 一个煮熟的鸡蛋有98摄氏度&#xff0c;将它放在18摄氏度的水池中&#xff0c;五分钟后鸡蛋的温度为38摄氏度&#x…

开源 2 + 1 链动模式、AI 智能名片、S2B2C 商城小程序在用户留存与品牌发展中的应用研究

摘要&#xff1a;本文以企业和个人品牌发展中至关重要的用户留存问题为切入点&#xff0c;结合管理大师彼得德鲁克对于企业兴旺发达的观点&#xff0c;阐述了用户留存对品牌营收的关键意义。在此基础上&#xff0c;深入分析开源 2 1 链动模式、AI 智能名片、S2B2C 商城小程序在…

搭建Python2和Python3虚拟环境

搭建Python3虚拟环境 1. 更新pip2. 搭建Python3虚拟环境第一步&#xff1a;安装python虚拟化工具第二步&#xff1a; 创建虚拟环境 3. 搭建Python2虚拟环境第一步&#xff1a;安装虚拟环境模块第二步&#xff1a;创建虚拟环境 4. workon命令管理虚拟机第一步&#xff1a;安装扩…

对接阿里云实人认证

对接阿里云实人认证-身份二要素核验接口整理 目录 应用场景 接口文档 接口信息 请求参数 响应参数 调试 阿里云openApi平台调试 查看调用结果 查看SDK示例 下载SDK 遇到问题 本地调试 总结 应用场景 项目有一个提现的场景&#xff0c;需要用户真实的身份信息。 …

基于卷积神经网络的车辆损坏部位检测系统带gui

项目源码获取方式见文章末尾&#xff01; 600多个深度学习项目资料&#xff0c;快来加入社群一起学习吧。 《------往期经典推荐------》 项目名称 1.【基于CNN-RNN的影像报告生成】 2.【卫星图像道路检测DeepLabV3Plus模型】 3.【GAN模型实现二次元头像生成】 4.【CNN模型实现…

CHI atomics 传输——CHI(6)

原子事务&#xff0c;指的是此事务就像原子一样是不可分割的&#xff0c;要么所有操作全部完成&#xff0c;要么全部不执行&#xff0c;不存在执行部分操作的情况。 ALU (Arithmetic Logic Unit) 算术逻辑单元&#xff0c;ALU在HN或SN AddrData&#xff1a;当前memory中的数据…

批量从Excel某一列中找到符合要求的值并提取其对应数据

本文介绍在Excel中&#xff0c;从某一列数据中找到与已知数据对应的字段&#xff0c;并提取这个字段对应数值的方法。 首先&#xff0c;来明确一下我们的需求。现在已知一个Excel数据&#xff0c;假设其中W列包含了上海市全部社区的名称&#xff0c;而其后的Y列则是这些社区对应…

有趣的Midjourney作品赏析(附提示词)

中文提示词&#xff1a;国风少年 C4D软件,高分辨率,超细节,超现实主义, 英文提示词&#xff1a;National Style Youth Cinema4D,high resolution,hyper detailed,surrealism, --niji 6 --ar 1:1 中文提示词&#xff1a;粘土模型&#xff0c;男性穿着中世纪欧洲蓝色盔甲&#x…