下面会贴出组件代码以及一个Demo,上面的效果图即为Demo的效果,建议直接将两份代码拷贝到自己的开发环境直接运行调试。
<template>
<!-- 鼠标画矩形选择对象 -->
<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>
</template>
<script lang="ts" setup>
import { reactive, toRefs, ref, computed } from "vue";
const props = withDefaults(
defineProps<{
objectClassName: string; // 选择对象的class name,用于定义如何获取对象
objectIdName: string; // 选择对象的id name,用于定义如何获取对象的id
selectObjectIds?: Array<string>; // 选中的对象ID
selectObjects?: Array<HTMLElement>; // 选中的对象
useCtrlSelect?: boolean; // 是否支持按住Ctrl多选
}>(),
{
useCtrlSelect: true // 默认支持按住Ctrl多选
}
);
const objectsRef = ref();
const maskRef = ref();
const emits = defineEmits(["update:selectObjects", "update:selectObjectIds"]);
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 handleMouseDown = event => {
// 展示矩形框,通过坐标位置来画出矩形
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 => {
maskPosition.value.endX = event.clientX;
maskPosition.value.endY = event.clientY;
};
/** 鼠标抬起离开 */
const handleMouseUp = () => {
// 移除鼠标监听事件
objectsRef.value.removeEventListener("mousemove", handleMouseMove);
objectsRef.value.removeEventListener("mouseup", handleMouseUp);
maskPosition.value.show = false;
handleResetMaskPosition();
handleGetSelectObject();
};
/** 获取选择的对象 */
const handleGetSelectObject = () => {
// 选中对象ID和对象元素
let tempSelectObjectIds: Array<string> = [];
let tempSelectObjects: Array<HTMLElement> = [];
// 如果按下了Ctrl键,之前选择的数据不清空
if (isPressCtrlKey.value) {
tempSelectObjectIds =
props.selectObjectIds === undefined ? [] : props.selectObjectIds;
tempSelectObjects =
props.selectObjects === undefined ? [] : props.selectObjects;
}
// 获取鼠标画出的矩形框位置
const rectanglePosition = maskRef.value.getClientRects()[0];
// 获取所有选择区域的对象; 这里获取的元素的方式定义于父组件的objectClassName
const selectedObjects = objectsRef.value.querySelectorAll(
`.${props.objectClassName}`
);
// 遍历对象,获取到每个对象的坐标位置,判断该位置是否在上面获取到的鼠标画矩形的框的位置中
selectedObjects.forEach(item => {
const objectPosition = item.getClientRects()[0];
// 这里获取的id的方式定义于父组件的objectIdName
if (compareObjectPosition(objectPosition, rectanglePosition)) {
const id = item.getAttribute(props.objectIdName);
// 如果按下了Ctrl键
if (isPressCtrlKey.value) {
// 已被选中的需要被取消选中
if (tempSelectObjectIds.includes(id)) {
tempSelectObjectIds = tempSelectObjectIds.filter(a => a != id);
tempSelectObjects = tempSelectObjects.filter(a => a != item);
} else {
tempSelectObjectIds.push(id);
tempSelectObjects.push(item);
}
} else {
tempSelectObjectIds.push(id);
tempSelectObjects.push(item);
}
}
});
// 回传到父组件
emits("update:selectObjects", tempSelectObjects);
emits("update:selectObjectIds", tempSelectObjectIds);
};
/**
* 判断对象坐标是否在鼠标画出的矩形框坐标位置内
* @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;`;
});
</script>
<style scoped lang="scss">
.objects {
height: 100%;
width: 100%;
overflow-y: auto;
.mask {
position: fixed;
background: #409eff;
opacity: 0.4;
z-index: 100;
}
}
</style>
建议直接将上面组件命名为 MouseDrawRectangle
<template>
<!------------- 鼠标画矩形选择对象组件DEMO,可以直接拷贝到你的页面去运行----------------------->
<div class="content">
<!--
MouseDrawRectangle说明:
objectClassName绑定到下面对象class名称;
objectIdName名称对应object_id;
useCtrlSelect默认是打开的,用于按住Ctrl键进行多选,以及取消已选择的对象。
selectObjectIds会实时从子组件更新过来,监听它的值来控制页面的选择状态即可。
另外有参数selectObjects会实时从子组件传回被选中的对象Dom信息
-->
<MouseDrawRectangle
objectClassName="select_object"
objectIdName="object_id"
:useCtrlSelect="true"
v-model:selectObjectIds="selectObjectIds"
v-model:selectObjects="selectObjects"
>
<!-- 这个是插槽,将业务内容的Dom限制在MouseDrawRectangle组件内,
这样可以将后面组件所有的监听事件绑定到组件上而不是整个页面Dom上,
鼠标滑动的区域也会限制死在组件内,而不是整个页面的范围 -->
<template #selcetObject>
<div class="objects_content">
<!-- 每一个选择的目标对象 -->
<div
v-for="item in 50"
:key="item"
class="select_object"
:object_id="item"
:class="
selectObjectIds.includes(item.toString()) ? 'is_selected' : ''
"
>
{{ item }}
</div>
</div>
</template>
</MouseDrawRectangle>
</div>
</template>
<script lang="ts" setup>
import { reactive, toRefs, watch } from "vue";
import MouseDrawRectangle from "@/components/objectSelect/mouseDrawRectangle.vue";
const state = reactive({
selectObjectIds: [] as Array<string>, // 选中的对象ID
selectObjects: [] as Array<HTMLElement> // 选中的对象DOM
});
const { selectObjectIds, selectObjects } = toRefs(state);
watch(
() => [selectObjectIds.value, selectObjects.value],
() => {
console.log("选中的ID=>", selectObjectIds);
console.log("选中的Dom=>", selectObjects);
}
);
</script>
<style scoped lang="scss">
.content {
// 因为使用flex布局,最下面一行盒子换行只会出现一半的高度,这里最好减去下每个盒子的高度
height: calc(100% - 50px);
overflow-y: auto;
padding: 20px;
.objects_content {
user-select: none;
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 10px;
// 盒子样式
> div {
width: 200px;
height: 100px;
background-color: #999;
}
.is_selected {
color: #fff;
box-sizing: border-box;
border: 3px #317aff solid;
border-radius: 5px;
}
}
}
</style>