先给大家看一下效果图
源代码
<template>
<div style="width: 45%">
<div style="width: 100%">
<div class="time">
<div class="timeleft">星期/时间</div>
<div class="timeright">
<div class="timeright_cell">
<el-row :gutter="0">
<el-col :span="12">
<div class="topitem" style="font-size: 12px; font-weight: bold">
00:00~12:00
</div>
</el-col>
<el-col :span="12">
<div class="topitem" style="font-size: 12px; font-weight: bold">
12:00~24:00
</div>
</el-col>
</el-row>
</div>
<div class="timeright_cell" style="color: #333333">
<el-row :gutter="0">
<el-col
:span="1"
v-for="(item, index) in 24"
:key="item"
@click="clicktime(index)"
>
<div
class="topitem"
:style="{ '--bgColor': bgColor }"
:class="{ is_selected: isTimeSelected(index) }"
>
{{ index }}
</div>
</el-col>
</el-row>
</div>
</div>
</div>
<div class="time">
<div style="width: 8.8%">
<div
class="timelefts"
:style="{ '--bgColor': bgColor }"
:class="{ is_selected: isDaySelected(index) }"
v-for="(dayName, index) in [
'星期一',
'星期二',
'星期三',
'星期四',
'星期五',
'星期六',
'星期日'
]"
:key="dayName"
@click="clickDay(index)"
>
{{ dayName }}
</div>
</div>
<div class="objects" ref="objectsRef" @mousedown="handleMouseDown">
<!-- 矩形选择框 -->
<div
class="mask"
ref="maskRef"
v-show="maskPosition.show"
:style="
'width:' +
maskWidth +
'left:' +
maskLeft +
'height:' +
maskHeight +
'top:' +
maskTop
"
/>
<!-- 选择对象内容的目标插槽 -->
<!-- <slot name="selcetObject" /> -->
<div class="objects_content">
<div
v-for="item in weekData"
:key="item.id"
class="select_object"
:day_index="item.daynum"
:time_index="item.timenum"
:object_id="item.id"
:class="{ is_selected: item.status }"
:style="{ '--bgColor': bgColor }"
>
<!-- {{ item.id }} -->
</div>
</div>
</div>
</div>
<!-- 鼠标画矩形选择对象 -->
</div>
<div class="box">
<div>
<div class="dt">已选择时间段</div>
<!-- 按钮-->
<el-button v-for="button in buttons" :key="button.text" :type="button.type" link>
{{ button.text }}
</el-button>
</div>
<div class="dtime">
<ul
v-for="item in weekData
.filter((t) => t.status == true)
.reduce(
(prev, curr) =>
prev.find((t) => t.daynum == curr.daynum) ? prev : [...prev, curr],
[]
)"
>
星期{{
convertSum(item.daynum)
}}:
<span class="dts" v-for="item in tList(item.daynum)">
{{ convertTime(item) }}
<!--{{ item.timenum - 1 }}:{{ item.id % 2 == 0 ? "30" : "00" }} ~{{ item.timenum - 1 }}:{{ item.id % 1 == 0 ? "30" : "00" }}-->
</span>
</ul>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { reactive, toRefs, ref, computed, onMounted, onBeforeMount } from "vue";
//数据格式1:默认0或者1,数据格式2:json字符串(x-(1-7)对应周一到周日), 如timeSet=”{‘1’:‘1,2,3’}”表示周一的0:30-1:00,1:00-1:30,1:30-2:00为播放时间,数据格式3:字符串,xHHmm x为1-7对应周一到周日,HH为小时,mm,00或30 00表示00-30这半小时, 30表示30-00这半小时
// 数据格式2:
// 格式为json字符串,(x-(1-7)对应周一到周日), 如timeSet=”{‘1’:‘1,2,3’}”表示周一的0:30-1:00,1:00-1:30,1:30-2:00为播放时间,
// 数据格式3:
// 格式是字符串,xHHmm x为1-7对应周一到周日,HH为小时,mm,00或30 00表示00-30这半小时, 30表示30-00这半小时
// 10600,10630,10700,10730,10800,10830,10900,10930,11000,11030,11100,11130,11200,11230,11300,11330,11400,11430,11500,11530,11600,11630,11700,11730,11800,11830,11900,11930,12000,12030,12100,12130,12200,12230,12300,12330,20600,20630,20700,20730,20800,20830,20900,20930,21000,21030,21100,21130,21200,21230,21300
const clientX = ref(0);
const clientY = ref(0);
const props = withDefaults(
defineProps<{
datatype?: number; //数据类型
week?: String; //绑定数组
objectClassName?: string; // 选择对象的class name,用于定义如何获取对象
objectIdName?: string; // 选择对象的id name,用于定义如何获取对象的id
useCtrlSelect?: boolean; // 是否支持按住Ctrl多选
bgColor?: string; //选中的背景色
}>(),
{
datatype: 1,
objectClassName: "select_object",
objectIdName: "object_id",
bgColor: "#197afb",
useCtrlSelect: false // 默认支持按住Ctrl多选
}
);
function findTimeInterval(arr) {
if (arr.length === 0) return [];
let intervals = [];
let startTime = arr[0];
let endTime = arr[0];
for (let i = 1; i < arr.length; i++) {
if (arr[i] === endTime + 1) {
endTime = arr[i];
} else {
intervals.push({ startTime: startTime, endTime: endTime });
startTime = arr[i];
endTime = arr[i];
}
}
intervals.push({ startTime: startTime, endTime: endTime });
return intervals;
}
const convertTime = (items) => {
let result = "";
// <!--{{ item.timenum - 1 }}:{{ item.id % 2 == 0 ? "30" : "00" }} ~{{ item.timenum - 1 }}:{{ item.id % 1 == 0 ? "30" : "00" }}-->
let list = items.map((el) => el.id);
let convertList = findTimeInterval(list);
console.log("转换后的", convertList);
if (convertList?.length > 0) {
convertList.forEach((el) => {
let start = items.find((item) => el.startTime == item.id);
let end = items.find((item) => el.endTime == item.id);
console.log("end", end);
if (el.startTime == el.endTime) {
let startStr = `${formatNum(start.timenum - 1)} : ${start.id % 2 == 0 ? "30" : "00"} `;
let endStr = "";
if (end.id % 2 != 0) {
endStr = `${formatNum(end.timenum - 1)} : 30`;
} else {
endStr = `${formatNum(end.timenum)} : 00`;
}
result += `${startStr} ~ ${endStr}`;
} else {
// 不一致 的
let endStr = "";
let startStr = `${formatNum(start.timenum - 1)} : ${start.id % 2 == 0 ? "30" : "00"} `;
// 结束时间存在问题, 单个多半个小时
if (el.endTime % 2 == 0) {
endStr = `${formatNum(end.timenum)} : 00`;
} else {
endStr = `${formatNum(end.timenum - 1)} : 30`;
}
result += `${startStr} ~ ${endStr}`;
}
});
}
return result;
};
const formatNum = (num) => {
return num < 10 ? `0${num}` : num;
}
/**
* 转换星期
* @param daynum
*/
const convertSum = (daynum) => {
switch (daynum) {
case 1:
return "一";
case 2:
return "二";
case 3:
return "三";
case 4:
return "四";
case 5:
return "五";
case 6:
return "六";
case 7:
return "日";
}
};
const tempStr = ref([]);
const tempStrComputed = computed(() => {
return JSON.stringify(tempStr);
});
const tList = (daynum) => {
let tArr = [];
let tItem = [];
let dayList = weekData.value
.filter((t) => t.status == true)
.reduce(
(prev, curr) => (prev.find((t) => t.daynum == curr.daynum) ? prev : [...prev, curr]),
[]
);
let dayAllDataList = weekData.value.filter((t) => t.daynum == daynum);
for (let i = 0; i < dayAllDataList.length; i++) {
let item = dayAllDataList[i];
if (item.status == false) {
if (tItem.length > 0) {
tArr.push(new Array(...tItem));
tItem.length = 0;
}
} else tItem.push(item);
}
if (tItem.length > 0) {
tArr.push(new Array(...tItem));
tItem.length = 0;
}
return tArr;
};
// const tList = computed(() => {
// console.log('tList');
// let tArr = [];
// let tItem = [];
// let dayList = weekData.value.filter(t=>t.status==true).reduce((prev, curr) => prev.find(t=>t.daynum == curr.daynum) ? prev : [...prev, curr], [])
// let dayAllDataList = weekData.value.filter(t=>t.daynum==2);
//
// for (let i = 0; i < dayAllDataList; i++) {
// let item = dayAllDataList[i];
// if(item.status==false){
// if (tItem.length > 0) {
// tArr.push(tItem);
// tItem.length = 0;
// }
// }else
// tItem.push(item);
// }
// return tArr;
// })
const hasHandleObjectIds = ref<number[]>([]); //已处理id数组
// 全局计数器
let uniqueId = 1;
// 初始化 weekData
const weekData = ref([] as any);
// 初始化 weekData 中的 hours
onMounted(() => {
for (let i = 0; i < 7; i++) {
for (let hour = 0, timenum = 1; hour < 48; hour += 2) {
weekData.value.push({ id: uniqueId++, status: false, daynum: i + 1, timenum: timenum });
weekData.value.push({ id: uniqueId++, status: false, daynum: i + 1, timenum: timenum });
timenum = (timenum % 24) + 1; // 使 timenum 在 1 到 24 之间循环
}
}
if (props.week) {
datahandle();
}
});
//初始化处理数据
const datahandle = () => {
let data: any = props.week === undefined ? [] : props.week;
if (props.datatype == 1) {
for (let i = 0; i < data.length; i++) {
if (Number(data[i]) == 1) {
weekData.value[i].status = true;
}
}
} else if (props.datatype == 2) {
let obj: any = JSON.parse(data);
for (const key in obj) {
let value = obj[key].split(",");
for (let i = 0; i < weekData.value.length; i++) {
const element = weekData.value[i];
if (element.daynum == key) {
if (value.includes(String(i))) {
weekData.value[i].status = true;
}
}
}
}
} else if (props.datatype == 3) {
let array = data.split(",");
for (let i = 0; i < array.length; i++) {
const element = array[i];
const dayNum = Number(element.slice(0, 1));
const hourNum = Number(element.slice(1, 3).replace(/\b(0+)/gi, ""));
const timeNum = element.slice(3, 5);
for (let i = 0; i < weekData.value.length; i++) {
const element = weekData.value[i];
if (element.daynum == dayNum && element.timenum == hourNum) {
if (timeNum == "00") {
if (i % 2 === 0) {
weekData.value[i].status = true;
}
} else {
if (i % 2 !== 0) {
weekData.value[i].status = true;
}
}
}
}
}
}
};
const isDaySelected = computed(() => {
return (index) => {
if (weekData.value && weekData.value.length > 0) {
return weekData.value
.filter((t) => t.daynum === index + 1)
.every((item) => item.status === true);
}
return false;
};
});
const isTimeSelected = computed(() => {
return (index) => {
if (weekData.value && weekData.value.length > 0) {
return weekData.value
.filter((t) => t.timenum === index + 1)
.every((item) => item.status === true);
}
return false;
};
});
const objectsRef = ref();
const maskRef = ref();
const emits = defineEmits(["update:week"]);
const state = reactive({
maskPosition: {
show: false,
startX: 0,
startY: 0,
endX: 0,
endY: 0
}, // 矩形框位置
isPressCtrlKey: false // 是否按下了Ctrl键
});
const { maskPosition, isPressCtrlKey } = toRefs(state);
// 若支持按住Ctrl多选,监听Ctrl事件
if (props.useCtrlSelect) {
// 释放
document.addEventListener("keyup", (event) => {
if (event.keyCode === 17) {
isPressCtrlKey.value = false;
}
});
// 按下
document.addEventListener("keydown", (event) => {
if (event.keyCode === 17) {
isPressCtrlKey.value = true;
}
});
}
//点击星期天数的事件
const clickDay = (index) => {
// var weekData: Array<any> = [];
// weekData = weekData === undefined ? [] : weekData;
// const hours = weekData[index].hours;
// const allIn = hasHandleObjectIds.value.every(item => weekData.includes(item));
const arr = weekData.value.filter((t) => t.daynum == index + 1);
const allIn = arr.every((item) => item.status === true);
if (allIn) {
arr.forEach((e) => {
e.status = false;
hasHandleObjectIds.value.splice(
hasHandleObjectIds.value.findIndex((t) => t == e.id),
1
);
});
} else {
arr.forEach((e) => {
e.status = true;
hasHandleObjectIds.value.push(e.id);
});
}
emits("update:week", handlerdata());
};
// 点击时间点数
const clicktime = (index) => {
const arr = weekData.value.filter((t) => t.timenum == index + 1);
const allIn = arr.every((item) => item.status === true);
if (allIn) {
arr.forEach((e) => {
e.status = false;
hasHandleObjectIds.value.splice(
hasHandleObjectIds.value.findIndex((t) => t == e.id),
1
);
});
} else {
arr.forEach((e) => {
e.status = true;
hasHandleObjectIds.value.push(e.id);
});
}
emits("update:week", handlerdata());
};
/** 鼠标按下 */
const handleMouseDown = (event) => {
//点下时清空已处理数组
hasHandleObjectIds.value.length = 0;
var id = Number(event.target.getAttribute(props.objectIdName));
// var weekData: Array<any> = [];
// weekData = weekData === undefined ? [] : weekData;
// const hourItem = weekData.flatMap(day => day.hours).find(item => item.id === id);
const index = weekData.value.findIndex((t) => t.id == id);
if (index > -1) {
weekData.value[index].status = !weekData.value[index].status;
}
if (!hasHandleObjectIds.value.includes(id)) {
hasHandleObjectIds.value.push(id);
} else {
hasHandleObjectIds.value.splice(
hasHandleObjectIds.value.findIndex((t) => t == id),
1
);
}
// 展示矩形框,通过坐标位置来画出矩形
maskPosition.value.show = true;
maskPosition.value.startX = event.clientX;
maskPosition.value.startY = event.clientY;
maskPosition.value.endX = event.clientX;
maskPosition.value.endY = event.clientY;
// 监听鼠标移动事件和抬起离开事件
objectsRef.value.addEventListener("mousemove", handleMouseMove);
objectsRef.value.addEventListener("mouseup", handleMouseUp);
};
/** 鼠标移动 */
const handleMouseMove = (event) => {
if (clientX.value !== event.clientX || clientY.value !== event.clientY) {
clientX.value = event.clientX;
clientY.value = event.clientY;
maskPosition.value.endX = event.clientX;
maskPosition.value.endY = event.clientY;
// var weekData: Array<any> = [];
// weekData = props.weekData === undefined ? [] : props.weekData;
const selectedObjects = objectsRef.value.querySelectorAll(`.${props.objectClassName}`);
// 获取鼠标画出的矩形框位置
const rectanglePosition = maskRef.value.getClientRects()[0];
var rectangleSelObjects: Array<number> = []; //矩形框内的id数组
selectedObjects.forEach((item) => {
const objectPosition = item.getClientRects()[0];
// 这里获取的id的方式定义于父组件的objectIdName
if (compareObjectPosition(objectPosition, rectanglePosition)) {
let id = item.getAttribute(props.objectIdName);
rectangleSelObjects.push(Number(id));
}
});
let handle = (id: number) => {
const index = weekData.value.findIndex((t) => t.id == id);
if (index > -1) weekData.value[index].status = !weekData.value[index].status;
// let index = tempSelectObjectIds.findIndex(t => t == id);
};
// 处理存在于 hasHandleObjectIds 中但不在 rectangleSelObjects 中的元素
for (let i = hasHandleObjectIds.value.length - 1; i >= 0; i--) {
const id = hasHandleObjectIds.value[i];
if (!rectangleSelObjects.includes(id)) {
handle(id);
hasHandleObjectIds.value.splice(i, 1);
}
}
// 处理存在于 rectangleSelObjects 中但不在 hasHandleObjectIds 中的元素
for (const id of rectangleSelObjects) {
if (!hasHandleObjectIds.value.includes(id)) {
handle(id);
hasHandleObjectIds.value.push(id);
}
}
// emits("update:weekData", weekData);
// emits("update:selectObjectIds", tempSelectObjectIds);
}
};
/** 鼠标抬起离开 */
const handleMouseUp = () => {
// 移除鼠标监听事件
objectsRef.value.removeEventListener("mousemove", handleMouseMove);
objectsRef.value.removeEventListener("mouseup", handleMouseUp);
maskPosition.value.show = false;
handleResetMaskPosition();
emits("update:week", handlerdata());
};
const handlerdata = () => {
let arr = weekData.value;
tempStr.value.push(Date.now() + "");
if (props.datatype == 1) {
let array = [] as any;
for (let i = 0; i < arr.length; i++) {
const element = arr[i];
if (element.status) {
array.push(1);
} else {
array.push(0);
}
}
return array.join("");
}
if (props.datatype == 2) {
let obj = {};
for (let i = 0; i < arr.length; i++) {
const element = arr[i];
if (element.status) {
if (obj[element.daynum]) {
obj[element.daynum].push(i);
} else {
obj[element.daynum] = [i];
}
}
}
for (let key in obj) {
if (Array.isArray(obj[key])) {
obj[key] = obj[key].join(","); // 使用逗号连接数组中的元素
}
}
// console.log(JSON.stringify(obj));
return JSON.stringify(obj);
}
if (props.datatype == 3) {
let array = [] as any;
for (let i = 0; i < arr.length; i++) {
const element = arr[i];
if (element.status) {
let day = String(element.daynum);
let time = element.timenum < 10 ? "0" + element.timenum : String(element.timenum);
let finalStr;
if (i % 2 === 0) {
finalStr = day + time + "00";
} else {
finalStr = day + time + "30";
}
array.push(finalStr);
}
}
return array.toString();
}
// return arr.filter(item => item.status == true);
};
/**
* 判断对象坐标是否在鼠标画出的矩形框坐标位置内
* @param objectPosition 对象坐标位置
* @param rectanglePosition 鼠标画出的矩形框坐标位置
*/
const compareObjectPosition = (objectPosition, rectanglePosition) => {
const maxX = Math.max(
objectPosition.x + objectPosition.width,
rectanglePosition.x + rectanglePosition.width
);
const maxY = Math.max(
objectPosition.y + objectPosition.height,
rectanglePosition.y + rectanglePosition.height
);
const minX = Math.min(objectPosition.x, rectanglePosition.x);
const minY = Math.min(objectPosition.y, rectanglePosition.y);
return (
maxX - minX <= objectPosition.width + rectanglePosition.width &&
maxY - minY <= objectPosition.height + rectanglePosition.height
);
};
/** 重置鼠标位置 */
const handleResetMaskPosition = () => {
maskPosition.value.startX = 0;
maskPosition.value.startY = 0;
maskPosition.value.endX = 0;
maskPosition.value.endY = 0;
};
/** 通过鼠标位置实时计算矩形框大小 */
const maskWidth = computed(() => {
return `${Math.abs(maskPosition.value.endX - maskPosition.value.startX)}px;`;
});
const maskHeight = computed(() => {
return `${Math.abs(maskPosition.value.endY - maskPosition.value.startY)}px;`;
});
const maskLeft = computed(() => {
return `${Math.min(maskPosition.value.startX, maskPosition.value.endX)}px;`;
});
const maskTop = computed(() => {
return `${Math.min(maskPosition.value.startY, maskPosition.value.endY)}px;`;
});
const buttons = [{ type: "primary", text: "清空选择" }] as const;
</script>
<style scoped lang="scss">
.dtime {
line-height: 22px;
margin-top: 10px;
padding: 5px;
color: #6c757d;
font-size: 12px;
transform: translateY(-20px);
}
.dt {
line-height: 22px;
padding: 4px;
color: #6c757d;
font-size: 13px;
display: inline-block;
}
.dts {
color: #000000;
font-size: 12px;
padding: 15px;
}
.box {
width: 100%;
border-left: #909399 1px solid;
border-bottom: #909399 1px solid;
border-right: #909399 1px solid;
}
.el-button {
justify-content: flex-end;
margin-left: 90%;
display: inline-block;
line-height: 22px;
transform: translateY(-28px);
}
.time {
width: 100%;
display: flex;
align-items: center;
font-size: 10px;
color: #222222;
// border: #999 1px solid;
.timeleft {
width: 8.75%;
height: 45.8px;
display: flex;
align-items: center;
justify-content: center;
background: #f5f7fa;
border-bottom: #999 1px solid;
border-top: #999 1px solid;
border-left: #999 1px solid;
box-sizing: border-box;
font-size: 10px;
}
.timeright {
height: 44.9px;
background: #f5f7fa;
width: 100%;
border-right: #999 1px solid;
box-sizing: border-box;
border-top: #999 1px solid;
font-size: 10px;
.timeright_cell {
border-bottom: #999 1px solid;
box-sizing: border-box;
.topitem {
height: 21.5px;
display: flex;
align-items: center;
justify-content: center;
border-left: #999 1px solid;
text-align: center;
box-sizing: border-box;
}
}
}
.timelefts {
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background: #f5f7fa;
border-bottom: #999 1px solid;
border-left: #999 1px solid;
border-right: #999 1px solid;
box-sizing: border-box;
}
.is_selected {
background: var(--bgColor);
color: #fff;
}
.objects {
height: 100%;
width: 100%;
// overflow-y: auto;
.mask {
position: fixed;
background: #409eff;
opacity: 0;
z-index: 100;
}
.objects_content {
user-select: none;
display: flex;
flex-wrap: wrap;
div {
display: flex;
align-items: center;
justify-content: center;
width: 2.083%;
height: 40px;
box-sizing: border-box;
border-bottom: #999 1px solid;
border-right: #999 1px solid;
}
}
}
}
</style>