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>
相关推荐
却尘3 分钟前
Next.js 请求最佳实践 - vercel 2026一月发布指南
前端·react.js·next.js
ccnocare4 分钟前
浅浅看一下设计模式
前端
Lee川8 分钟前
🎬 从标签到屏幕:揭秘现代网页构建与适配之道
前端·面试
Ticnix34 分钟前
ECharts初始化、销毁、resize 适配组件封装(含完整封装代码)
前端·echarts
纯爱掌门人38 分钟前
终焉轮回里,藏着 AI 与人类的答案
前端·人工智能·aigc
twl41 分钟前
OpenClaw 深度技术解析
前端
崔庆才丨静觅1 小时前
比官方便宜一半以上!Grok API 申请及使用
前端
星光不问赶路人1 小时前
vue3使用jsx语法详解
前端·vue.js
天蓝色的鱼鱼1 小时前
shadcn/ui,给你一个真正可控的UI组件库
前端
布列瑟农的星空1 小时前
前端都能看懂的Rust入门教程(三)——控制流语句
前端·后端·rust