Vue鼠标右键画矩形和Ctrl按键多选组件

效果图

说明

下面会贴出组件代码以及一个Demo,上面的效果图即为Demo的效果,建议直接将两份代码拷贝到自己的开发环境直接运行调试。

组件代码

javascript 复制代码
<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

javascript 复制代码
<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>
相关推荐
萧大侠jdeps1 小时前
Vue 3 与 Tauri 集成开发跨端APP
前端·javascript·vue.js·tauri
JYeontu2 小时前
实现一个动态脱敏指令,输入时候显示真实数据,展示的时候进行脱敏
前端·javascript·vue.js
发呆的薇薇°2 小时前
react里使用Day.js显示时间
前端·javascript·react.js
嘤嘤嘤2 小时前
基于大模型技术构建的 GitHub Assistant
前端·github
KeepCatch2 小时前
CSS 动画与过渡效果
前端
跑跑快跑2 小时前
React vite + less
前端·react.js·less
web136885658712 小时前
ctfshow_web入门_命令执行_web29-web39
前端
GISer_Jing2 小时前
前端面试题合集(一)——HTML/CSS/Javascript/ES6
前端·javascript·html
清岚_lxn2 小时前
es6 字符串每隔几个中间插入一个逗号
前端·javascript·算法
胡西风_foxww2 小时前
【ES6复习笔记】Map(14)
前端·笔记·es6·map