星期-时间范围选择器 滑动选择时间 最小粒度 vue3
根据业务需要,实现了一个可选择时间范围的周视图。用户可以通过鼠标拖动来选择时间段,并且可以通过快速选择组件来快速选择特定的时间范围。
功能介绍
- 时间范围选择:用户可以通过鼠标拖动来选择时间段。
- 快速选择:提供快速选择组件,用户可以通过点击快速选择特定的时间范围,如上午、下午、工作日、周末等。
- 自定义样式:可以通过
selectionColor
属性自定义选中区域的颜色。 - 数据绑定:通过
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)!