Vue鼠标右键画矩形和Ctrl按键多选组件方式
更新时间:2024年12月26日 09:02:40 作者:-小龙人
文章介绍了一个Vue组件,该组件允许用户通过鼠标右键在画布上绘制矩形,并且支持通过Ctrl键进行多选,文章附带了组件代码和一个示例,建议读者将代码复制到自己的开发环境中进行调试
Vue鼠标右键画矩形和Ctrl按键多选组件
效果图
说明
下面会贴出组件代码以及一个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>
Demo
建议直接将上面组件命名为 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>
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。
相关文章
vue组件 keep-alive 和 transition 使用详解
这篇文章主要介绍了vue组件 keep-alive 和 transition 使用详解,需要的朋友可以参考下2019-10-10vue如何导出文件流获取附件名称并下载(在response.headers里解析filename导出)
这篇文章主要介绍了vue如何导出文件流获取附件名称并下载(在response.headers里解析filename导出),具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教2024-07-07在vue项目中集成graphql(vue-ApolloClient)
这篇文章主要介绍了在vue项目中集成graphql(vue-ApolloClient),小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧2018-09-09
最新评论