Vue鼠標(biāo)右鍵畫(huà)矩形和Ctrl按鍵多選組件方式
Vue鼠標(biāo)右鍵畫(huà)矩形和Ctrl按鍵多選組件
效果圖

說(shuō)明
下面會(huì)貼出組件代碼以及一個(gè)Demo,上面的效果圖即為Demo的效果,建議直接將兩份代碼拷貝到自己的開(kāi)發(fā)環(huán)境直接運(yùn)行調(diào)試。
組件代碼
<template>
<!-- 鼠標(biāo)畫(huà)矩形選擇對(duì)象 -->
<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
"
/>
<!-- 選擇對(duì)象內(nèi)容的目標(biāo)插槽 -->
<slot name="selcetObject" />
</div>
</template><script lang="ts" setup>
import { reactive, toRefs, ref, computed } from "vue";
const props = withDefaults(
defineProps<{
objectClassName: string; // 選擇對(duì)象的class name,用于定義如何獲取對(duì)象
objectIdName: string; // 選擇對(duì)象的id name,用于定義如何獲取對(duì)象的id
selectObjectIds?: Array<string>; // 選中的對(duì)象ID
selectObjects?: Array<HTMLElement>; // 選中的對(duì)象
useCtrlSelect?: boolean; // 是否支持按住Ctrl多選
}>(),
{
useCtrlSelect: true // 默認(rèn)支持按住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多選,監(jiān)聽(tīng)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;
}
});
}
/** 鼠標(biāo)按下 */
const handleMouseDown = event => {
// 展示矩形框,通過(guò)坐標(biāo)位置來(lái)畫(huà)出矩形
maskPosition.value.show = true;
maskPosition.value.startX = event.clientX;
maskPosition.value.startY = event.clientY;
maskPosition.value.endX = event.clientX;
maskPosition.value.endY = event.clientY;
// 監(jiān)聽(tīng)鼠標(biāo)移動(dòng)事件和抬起離開(kāi)事件
objectsRef.value.addEventListener("mousemove", handleMouseMove);
objectsRef.value.addEventListener("mouseup", handleMouseUp);
};
/** 鼠標(biāo)移動(dòng) */
const handleMouseMove = event => {
maskPosition.value.endX = event.clientX;
maskPosition.value.endY = event.clientY;
};
/** 鼠標(biāo)抬起離開(kāi) */
const handleMouseUp = () => {
// 移除鼠標(biāo)監(jiān)聽(tīng)事件
objectsRef.value.removeEventListener("mousemove", handleMouseMove);
objectsRef.value.removeEventListener("mouseup", handleMouseUp);
maskPosition.value.show = false;
handleResetMaskPosition();
handleGetSelectObject();
};
/** 獲取選擇的對(duì)象 */
const handleGetSelectObject = () => {
// 選中對(duì)象ID和對(duì)象元素
let tempSelectObjectIds: Array<string> = [];
let tempSelectObjects: Array<HTMLElement> = [];
// 如果按下了Ctrl鍵,之前選擇的數(shù)據(jù)不清空
if (isPressCtrlKey.value) {
tempSelectObjectIds =
props.selectObjectIds === undefined ? [] : props.selectObjectIds;
tempSelectObjects =
props.selectObjects === undefined ? [] : props.selectObjects;
}
// 獲取鼠標(biāo)畫(huà)出的矩形框位置
const rectanglePosition = maskRef.value.getClientRects()[0];
// 獲取所有選擇區(qū)域的對(duì)象; 這里獲取的元素的方式定義于父組件的objectClassName
const selectedObjects = objectsRef.value.querySelectorAll(
`.${props.objectClassName}`
);
// 遍歷對(duì)象,獲取到每個(gè)對(duì)象的坐標(biāo)位置,判斷該位置是否在上面獲取到的鼠標(biāo)畫(huà)矩形的框的位置中
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);
};
/**
* 判斷對(duì)象坐標(biāo)是否在鼠標(biāo)畫(huà)出的矩形框坐標(biāo)位置內(nèi)
* @param objectPosition 對(duì)象坐標(biāo)位置
* @param rectanglePosition 鼠標(biāo)畫(huà)出的矩形框坐標(biāo)位置
*/
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
);
};
/** 重置鼠標(biāo)位置 */
const handleResetMaskPosition = () => {
maskPosition.value.startX = 0;
maskPosition.value.startY = 0;
maskPosition.value.endX = 0;
maskPosition.value.endY = 0;
};
/** 通過(guò)鼠標(biāo)位置實(shí)時(shí)計(jì)算矩形框大小 */
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>
<!------------- 鼠標(biāo)畫(huà)矩形選擇對(duì)象組件DEMO,可以直接拷貝到你的頁(yè)面去運(yùn)行----------------------->
<div class="content">
<!--
MouseDrawRectangle說(shuō)明:
objectClassName綁定到下面對(duì)象class名稱;
objectIdName名稱對(duì)應(yīng)object_id;
useCtrlSelect默認(rèn)是打開(kāi)的,用于按住Ctrl鍵進(jìn)行多選,以及取消已選擇的對(duì)象。
selectObjectIds會(huì)實(shí)時(shí)從子組件更新過(guò)來(lái),監(jiān)聽(tīng)它的值來(lái)控制頁(yè)面的選擇狀態(tài)即可。
另外有參數(shù)selectObjects會(huì)實(shí)時(shí)從子組件傳回被選中的對(duì)象Dom信息
-->
<MouseDrawRectangle
objectClassName="select_object"
objectIdName="object_id"
:useCtrlSelect="true"
v-model:selectObjectIds="selectObjectIds"
v-model:selectObjects="selectObjects"
>
<!-- 這個(gè)是插槽,將業(yè)務(wù)內(nèi)容的Dom限制在MouseDrawRectangle組件內(nèi),
這樣可以將后面組件所有的監(jiān)聽(tīng)事件綁定到組件上而不是整個(gè)頁(yè)面Dom上,
鼠標(biāo)滑動(dòng)的區(qū)域也會(huì)限制死在組件內(nèi),而不是整個(gè)頁(yè)面的范圍 -->
<template #selcetObject>
<div class="objects_content">
<!-- 每一個(gè)選擇的目標(biāo)對(duì)象 -->
<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>, // 選中的對(duì)象ID
selectObjects: [] as Array<HTMLElement> // 選中的對(duì)象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 {
// 因?yàn)槭褂胒lex布局,最下面一行盒子換行只會(huì)出現(xiàn)一半的高度,這里最好減去下每個(gè)盒子的高度
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>總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Vue實(shí)現(xiàn)一個(gè)返回頂部backToTop組件
本篇文章主要介紹了Vue實(shí)現(xiàn)一個(gè)返回頂部backToTop組件,可以實(shí)現(xiàn)回到頂部效果,具有一定的參考價(jià)值,有興趣的可以了解一下2017-07-07
vue mintui-Loadmore結(jié)合實(shí)現(xiàn)下拉刷新和上拉加載示例
本篇文章主要介紹了vue mintui-Loadmore結(jié)合實(shí)現(xiàn)下拉刷新和上拉加載示例,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-10-10
vue-cli+webpack項(xiàng)目 修改項(xiàng)目名稱的方法
下面小編就為大家分享一篇vue-cli+webpack項(xiàng)目 修改項(xiàng)目名稱的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-02-02
基于vue-cli、elementUI的Vue超簡(jiǎn)單入門小例子(推薦)
這篇文章主要介紹了基于vue-cli、elementUI的Vue超簡(jiǎn)單入門小例子,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04
詳解vue中使用vue-quill-editor富文本小結(jié)(圖片上傳)
這篇文章主要介紹了詳解vue中使用vue-quill-editor富文本小結(jié)(圖片上傳),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04
vue路由篇之router-view內(nèi)容無(wú)法渲染出來(lái)問(wèn)題
這篇文章主要介紹了vue路由篇之router-view內(nèi)容無(wú)法渲染出來(lái)問(wèn)題及解決,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-04-04
用Vue.js在瀏覽器中實(shí)現(xiàn)裁剪圖像功能
在本教程中,我們將探討如何在瀏覽器中使用 JavaScript 庫(kù)來(lái)操作圖片,為服務(wù)器上的存儲(chǔ)做準(zhǔn)備,并在 Web 程序中使用。我們將使用 Vue.js 而不是原生 JavaScript來(lái)完成此操作,需要的朋友可以參考下2019-06-06

