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

news2024/11/24 17:16:13

效果图

效果图

说明

下面会贴出组件代码以及一个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>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1090930.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【AI视野·今日NLP 自然语言处理论文速览 第五十二期】Wed, 11 Oct 2023

AI视野今日CS.NLP 自然语言处理论文速览 Wed, 11 Oct 2023 Totally 81 papers &#x1f449;上期速览✈更多精彩请移步主页 Daily Computation and Language Papers LongLLMLingua: Accelerating and Enhancing LLMs in Long Context Scenarios via Prompt Compression Author…

Web3 招聘 | Bitget、MyShell、imToken、Arweave 多项目招聘中

「Web3 招聘」是 TinTinLand 为 Web3 项目和求职者创建的一个招聘信息汇集专栏。本栏目将持续更新区块链行业招聘信息&#xff0c;满足不同求职者与项目方的多样需求。欢迎各项目方联系 TinTinLand 发布职位需求&#xff0c;欢迎求职者关注 TinTinLand 获取最新招聘信息。 此外…

python(自5)scrapy下载安装 基本使用

一&#xff0c;安装下载 (1)安装步骤 ​ //安装包下载&#xff1a;Archived: Python Extension Packages for Windows - Christoph Gohlke (uci.edu) ​// 先下载对应的 twisted 然后 pip install 拖进twisted//例如&#xff1a; twisted_iocpsupport‑1.0.2‑cp311‑cp31…

人机交互中的信息数量与信息质量

在人机交互中&#xff0c;信息数量和信息质量是影响人机交互效果的两个重要因素。信息数量指的是系统向用户提供的信息总量&#xff0c;包括输入信息、反馈信息、展示信息、错误信息等&#xff0c;在合适的情况下越少越好&#xff1b;信息质量则是指信息的准确性、有效性、清晰…

十九、【减淡工具组】

文章目录 减淡工具加深工具海绵工具 减淡工具 减淡工具的作用就是把画笔涂抹过后的区域的颜色减淡&#xff0c;让这部分区域的颜色看起来更加白更加亮&#xff1a; 也可以采用新建空白图层&#xff0c;然后采用画笔工具&#xff0c;用涂抹中性灰的方式让其变亮&#xff0c;采…

将字符串转换为小写形式字符串.casefold()

【小白从小学Python、C、Java】 【计算机等级考试500强双证书】 【Python-数据分析】 将字符串转换为小写形式 字符串.casefold() 请问题目中的代码输出什么&#xff1f; s "Hello World" print("【显示】s ",s) print("【执行】s.casefold()"…

Python学习-----Day06——排序

冒泡排序 冒泡排序&#xff08;Bubble Sort&#xff09;也是一种简单直观的排序算法。它重复地走访过要排序的数列&#xff0c;一次比较两个元素&#xff0c;如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换&#xff0c;也就是说该数列已经…

C# InformativeDrawings 生成素描画

效果 项目 下载 可执行程序exe下载 源码下载

【Python-Django】基于TF-IDF算法的医疗推荐系统复现过程

复现步骤 step1&#xff1a; 修改原templates路径&#xff0c;删除&#xff0c;将setting.py中的路径置空 step2&#xff1a; 注册app python manage.py startapp [app名称]在app目录下创建static和templates目录 step3&#xff1a; 将项目中的资源文化进行拷贝 step4&#…

7 使用Docker容器管理的tomcat容器中的项目连接mysql数据库

1、查看容器的IP 1&#xff09;进入容器 docker exec -it mysql-test /bin/bash 2&#xff09;显示hosts文件内容 cat /etc/hosts 这里容器的ip为172.17.0.2 除了上面的方法外&#xff0c;也可以在容器外使用docker inspect查看容器的IP docker inspect mysql-test 以下为…

python openai宠物名字生成器

文章目录 OpenAICompletion宠物名字生成器提示词工程 prompt enginering 构建应用程序 OpenAI OpenAI 已经训练了非常擅长理解和生成文本的领先的语言模型。我们的 API 提供对这些模型的访问&#xff0c;可用于处理几乎任何涉及”语言处理“的任务。 Completion 补全&#x…

26 mysql 索引的存储更新删除

前言 这里来看一下 mysql 中索引的 增删改查 查询在前面的系列文章中都有使用到 这里 来看一下 增删改 的相关实现 索引记录 和 数据记录 的处理方式是一致的 索引的存储 创建数据表如下, 除了主键之外, 创建了一个 field1, field2 的一个联合索引 CREATE TABLE tz_…

【Python 零基础入门】Pandas

【Python 零基础入门】第七课 Pandas 【Python 零基础入门】第七课 PandasPandas 是什么?为什么 选择 PandasPandas 的特征Pandas 的应用场景Pandas 底层 安装 PandasSeries 数组什么是 Series?Series 创建 Series 数组操作数据检索数据修改过滤Series 数组运算总结 DataFram…

js对url进行编码解码的三种方案,JS在url中如何传递参数或特殊符号

为什么要进行url编码&#xff1f; 当你的URL里出现%2F 怎么办&#xff1f;JS在url中如何传递参数或者特殊符号呢&#xff1f;在url链接中会经常碰到一些%2F、%2B等特殊符号怎么解决呢&#xff1f;下面我们来了解一下&#xff1a; 根据RFC标准&#xff0c;有些符号在URI中是不…

OpenHarmony docker环境搭建

OpenHarmony docker环境搭建 要求一台安装ubuntu的虚拟机,vscode软件 安装docker 在 Ubuntu 上安装 Docker 非常直接。我们将会启用 Docker 软件源&#xff0c;导入 GPG key&#xff0c;并且安装软件包。 首先&#xff0c;更新软件包索引&#xff0c;并且安装必要的依赖软件…

运放的常见应用(收藏)

运放对于外人来说可能有点陌生&#xff0c;但它在我们生活中无处不在&#xff0c;运放的最基本电路符号&#xff1a; 01 放大器 1、反相放大器电路图 输入输出波形&#xff1a; 2、同相放大器&#xff1a; 输入输出波形&#xff1a; 3、电压跟随器 输入输出波形&#xff1a; 4、…

快速了解什么是jwt及如何使用jwt

一、导言 1、什么是jwt及组成部分 JWT&#xff08;JSON Web Token&#xff09;是一种用于在网络应用间安全传递声明&#xff08;claim&#xff09;的开放标准。它由三部分组成&#xff1a;头部&#xff08;Header&#xff09;、载荷&#xff08;Payload&#xff09;和签名&…

git cherry-pick命令

问题场景&#xff1a; 需要把dev分支的代码&#xff0c;合并到master分支中&#xff0c;但是又不能根据整个分支合并&#xff0c;所有使用cherry-pick命令&#xff0c;根据提交的commit号来合并 问题描述&#xff1a; 原因分析&#xff1a; 解决方案&#xff1a; 1.在dev分支…

2023年中国乘用车金属冲压件产量、需求量及行业市场规模分析[图]

汽车冲压件&#xff0c;主要是指通过压力机和冲压模具对金属材料施加外力&#xff0c;使之产生塑性变形或分离&#xff0c;从而获得所需形状和尺寸的工件&#xff0c;广泛应用于汽车覆盖件、白车身系统、座椅系统、仪表系统及排气系统等部件&#xff0c;汽车车身的金属件几乎全…

Redis第一章:初识

目录 1.1 Redis介绍 1.2 Redis 特性 1.3 Redis 使⽤场景 1.3.1 Redis 可以做什么 1.3.2 Redis 不可以做什么 1.4 安装并启动 Redis 1.5 Redis 命令⾏客⼾端 1.1 Redis介绍 Redis 是⼀种基于键值对&#xff08;key-value&#xff09;的 NoSQL 数据库&#xff0c;与很多键…