先给大家看一下效果图
源代码
<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)"><divclass="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%"><divclass="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"><!-- 矩形选择框 --><divclass="mask"ref="maskRef"v-show="maskPosition.show":style="'width:' +maskWidth +'left:' +maskLeft +'height:' +maskHeight +'top:' +maskTop"/><!-- 选择对象内容的目标插槽 --><!-- <slot name="selcetObject" /> --><div class="objects_content"><divv-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"><ulv-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,用于定义如何获取对象的iduseCtrlSelect?: 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的方式定义于父组件的objectIdNameif (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>