前端 图片上鼠标画矩形框,标注文字,任意删除

news2025/1/8 14:13:32

效果:

页面描述:

对给定的几张图片,每张能用鼠标在图上画框,标注相关文字,框的颜色和文字内容能自定义改变,能删除任意画过的框。

实现思路:

1、对给定的这几张图片,用分页器绑定展示,能选择图片;

2、图片上绑定事件@mousedown鼠标按下——开始画矩形、@mousemove鼠标移动——绘制中临时画矩形、@mouseup鼠标抬起——结束画矩形重新渲染;

开始画矩形:鼠标按下,记录鼠标按下的位置。遍历标签数组,找到check值为true的标签,用其样式和名字创建新的标签,加入该图片的矩形框们的数组。注意,监听鼠标如果是按下后马上抬起,结束标注。

更新矩形:识别到新的标签存在,鼠标移动时监听移动距离,更新当前矩形宽高,用canvas绘制实时临时矩形。

结束画矩形:刷新该图片的矩形框们的数组,触发重新渲染。

3、在图片上v-for遍历渲染矩形框,盒子绑定动态样式改变宽高;

4、右侧能添加、修改矩形框颜色和文字;

5、列举出每个矩形框名称,能选择进行删除,还能一次清空;

<template>
<div class="allbody">
      <div class="body-top">
        <button class="top-item2" @click="clearAnnotations">清空</button>
      </div>
      <div class="body-btn">
        <div class="btn-content">
          <div class="image-container">
            <!-- <img :src="imageUrl" alt="Character Image" /> -->
            <img :src="state.imageUrls[state.currentPage - 1]" @mousedown="startAnnotation" @mousemove="updateAnnotation" @mouseup="endAnnotation" />
            <!-- 使用canvas覆盖在图片上方,用于绘制临时矩形 -->
            <canvas ref="annotationCanvas"></canvas>
            <div v-for="annotation in annotations[state.currentPage - 1]" :key="annotation.id" class="annotation" :style="annotationStyle(annotation)">
              <div class="label">{{ annotation.label }}</div>
            </div>
          </div>
          <Pagination
            v-model:current="state.currentPage"
            v-model:page-size="state.pageSize"
            show-quick-jumper
            :total="state.imageUrls.length"
            :showSizeChanger="false"
            :show-total="total => `共 ${total} 张`" />
        </div>
        <div class="sidebar">
          <div class="sidebar-title">标签</div>
          <div class="tags">
            <div class="tags-item" v-for="(tags, index2) in state.tagsList" :key="index2" @click="checkTag(index2)">
              <div class="tags-checkbox">
                <div :class="tags.check === true ? 'checkbox-two' : 'notcheckbox-two'"></div>
              </div>
              <div class="tags-right">
                <input class="tags-color" type="color" v-model="tags.color" />
                <input type="type" class="tags-input" v-model="tags.name" />
                <button class="tags-not" @click="deleteTag(index2)"><DeleteOutlined style="color: #ff0202" /></button>
              </div>
            </div>
          </div>
          <div class="sidebar-btn">
            <button class="btn-left" @click="addTags()">添加</button>
          </div>
          <div class="sidebar-title">数据</div>
          <div class="sidebars">
            <div class="sidebar-item" v-for="(annotation, index) in annotations[state.currentPage - 1]" :key="annotation.id">
              <div class="sidebar-item-font">{{ index + 1 }}.{{ annotation.name }}</div>
              <button class="sidebar-item-icon" @click="removeAnnotation(annotation.id)"><DeleteOutlined style="color: #ff0202" /></button> </div
          ></div>
        </div>
      </div>
    </div>
</template>
<script lang="ts" setup>
  import { DeleteOutlined } from '@ant-design/icons-vue';
  import { Pagination } from 'ant-design-vue';

  interface State {
    tagsList: any;
    canvasX: number;
    canvasY: number;
    currentPage: number;
    pageSize: number;
    imageUrls: string[];
  };

  const state = reactive<State>({
    tagsList: [], // 标签列表
    canvasX: 0,
    canvasY: 0,
    currentPage: 1,
    pageSize: 1,
    imageUrls: [apiUrl.value + '/api/File/Image/annexpic/20241203Q9NHJ.jpg', apiUrl.value + '/api/file/Image/document/20241225QBYXZ.jpg'],
  });

  interface Annotation {
    id: string;
    name: string;
    x: number;
    y: number;
    width: number;
    height: number;
    color: string;
    label: string;
    border: string;
  };

  const annotations = reactive<Array<Annotation[]>>([[]]);
  let currentAnnotation: Annotation | null = null;

  //开始标注
  function startAnnotation(event: MouseEvent) {
    // 获取当前选中的标签
    var tagsCon = { id: 1, check: true, color: '#000000', name: '安全帽' };
    // 遍历标签列表,获取当前选中的标签
    for (var i = 0; i < state.tagsList.length; i++) {
      if (state.tagsList[i].check) {
        tagsCon.id = state.tagsList[i].id;
        tagsCon.check = state.tagsList[i].check;
        tagsCon.color = state.tagsList[i].color;
        tagsCon.name = state.tagsList[i].name;
      }
    }
    // 创建新的标注
    currentAnnotation = {
      id: crypto.randomUUID(),
      name: tagsCon.name,
      x: event.offsetX,
      y: event.offsetY,
      width: 0,
      height: 0,
      color: '#000000',
      label: (annotations[state.currentPage - 1].length || 0) + 1 + tagsCon.name,
      border: tagsCon.color,
    };
    annotations[state.currentPage - 1].push(currentAnnotation);

    //记录鼠标按下的位置
    state.canvasX = event.offsetX;
    state.canvasY = event.offsetY;

    //监听鼠标如果是按下后马上抬起,结束标注
    const mouseupHandler = () => {
      endAnnotation();
      window.removeEventListener('mouseup', mouseupHandler);
    };
    window.addEventListener('mouseup', mouseupHandler);
  }

  //更新标注
  function updateAnnotation(event: MouseEvent) {
    if (currentAnnotation) {
      //更新当前标注的宽高,为负数时,鼠标向左或向上移动
      currentAnnotation.width = event.offsetX - currentAnnotation.x;
      currentAnnotation.height = event.offsetY - currentAnnotation.y;
    }

    //如果正在绘制中,更新临时矩形的位置
    if (annotationCanvas.value) {
      const canvas = annotationCanvas.value;
      //取得类名为image-container的div的宽高
      const imageContainer = document.querySelector('.image-container');
      canvas.width = imageContainer?.clientWidth || 800;
      canvas.height = imageContainer?.clientHeight || 534;
      const context = canvas.getContext('2d');
      if (context) {
        context.clearRect(0, 0, canvas.width, canvas.height);
        context.strokeStyle = currentAnnotation?.border || '#000000';
        context.lineWidth = 2;
        context.strokeRect(state.canvasX, state.canvasY, currentAnnotation?.width || 0, currentAnnotation?.height || 0);
      }
    }
  }

  function endAnnotation() {
    //刷新annotations[state.currentPage - 1],触发重新渲染
    annotations[state.currentPage - 1] = annotations[state.currentPage - 1].slice();
    currentAnnotation = null;
  }

  function annotationStyle(annotation: Annotation) {
    //如果宽高为负数,需要调整left和top的位置
    const left = annotation.width < 0 ? annotation.x + annotation.width : annotation.x;
    const top = annotation.height < 0 ? annotation.y + annotation.height : annotation.y;
    return {
      left: `${left}px`,
      top: `${top}px`,
      width: `${Math.abs(annotation.width)}px`,
      height: `${Math.abs(annotation.height)}px`,
      border: `2px solid ${annotation.border}`,
    };
  }

  // 选择标签
  function checkTag(index2: number) {
    state.tagsList.forEach((item, index) => {
      if (index === index2) {
        item.check = true;
      } else {
        item.check = false;
      }
    });
  }

  // 删除标签
  function deleteTag(index: number) {
    state.tagsList.splice(index, 1);
  }

  function addTags() {
    state.tagsList.push({ id: state.tagsList.length + 1, check: false, color: '#000000', name: '' });
  }

  // 移除某个标注
  function removeAnnotation(id: string) {
    const index = annotations[state.currentPage - 1].findIndex(a => a.id === id);
    if (index !== -1) {
      annotations[state.currentPage - 1].splice(index, 1);
    }
  }

  // 清空所有标注
  function clearAnnotations() {
    annotations[state.currentPage - 1].splice(0, annotations[state.currentPage - 1].length);
  }

  onMounted(() => {
    for (let i = 0; i < state.imageUrls.length; i++) {
      annotations.push([]);
    }
  });

</script>
<style>
  .body-top {
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: center;
    margin-bottom: 10px;
    width: 85%;
  }
  .top-item1 {
    width: 70px;
    height: 28px;
    line-height: 26px;
    text-align: center;
    background-color: #028dff;
    border: 1px solid #028dff;
    border-radius: 5px;
    font-size: 14px;
    color: #fff;
    margin-left: 20px;
  }
  .top-item2 {
    width: 70px;
    height: 28px;
    line-height: 26px;
    text-align: center;
    background-color: rgb(255, 2, 2);
    border: 1px solid rgb(255, 2, 2);
    border-radius: 5px;
    font-size: 14px;
    color: #fff;
    margin-left: 20px;
  }
  .body-btn {
    margin: 0;
    padding: 10px 13px 0 0;
    min-height: 630px;
    display: flex;
    background-color: #f5f5f5;
  }
  .btn-content {
    flex-grow: 1;
    padding: 10px;
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    align-items: center;
  }
  .image-container {
    height: 500px;
    margin: 40px;
  }
  .image-container img {
    height: 500px !important;
  }
  .ant-pagination {
    margin-bottom: 18px;
  }
  .number-input {
    width: 70px;
    border: 1px solid #ccc;
    border-radius: 4px;
    text-align: center;
    font-size: 16px;
    background-color: #f9f9f9;
    outline: none;
    color: #66afe9;
  }
  .sidebar {
    display: flex;
    flex-direction: column;
    width: 280px;
    height: 640px;
    background-color: #fff;
    padding: 10px;
    border-radius: 7px;
  }
  .sidebar-title {
    font-size: 16px;
    font-weight: 600;
    margin-bottom: 10px;
  }
  .sidebars {
    overflow: auto;
  }
  .sidebar .tags {
    margin-bottom: 10px;
  }
  .tags-item {
    display: flex;
    flex-direction: row;
    align-items: center;
  }
  .tags-checkbox {
    width: 24px;
    height: 24px;
    border-radius: 50px;
    border: 1px solid #028dff;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    margin-right: 7px;
  }
  .checkbox-two {
    background-color: #028dff;
    width: 14px;
    height: 14px;
    border-radius: 50px;
  }
  .notcheckbox-two {
    width: 14px;
    height: 14px;
    border-radius: 50px;
    border: 1px solid #028dff;
  }
  .tags-right {
    display: flex;
    flex-direction: row;
    align-items: center;
    background-color: #f5f5f5;
    border-radius: 5px;
    padding: 5px;
    width: 90%;
  }
  .tags-color {
    width: 26px;
    height: 26px;
    border-radius: 5px;
  }
  .tags-input {
    border: 1px solid #fff;
    width: 153px;
    margin: 0 10px;
  }
  .tags-not {
    border: 1px solid #f5f5f5;
    font-size: 12px;
  }
  .sidebar-btn {
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: right;
  }
  .btn-left {
    width: 60px;
    height: 28px;
    line-height: 26px;
    text-align: center;
    border: 1px solid #028dff;
    border-radius: 5px;
    font-size: 14px;
    color: #028dff;
  }
  .btn-right {
    width: 60px;
    height: 28px;
    line-height: 26px;
    text-align: center;
    background-color: #028dff;
    border: 1px solid #028dff;
    border-radius: 5px;
    font-size: 14px;
    color: #fff;
    margin-left: 10px;
  }
  .sidebar-item {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding-right: 2px;
  }
  .sidebar-item-font {
    margin-right: 10px;
  }
  .sidebar-item-icon {
    font-size: 12px;
    border: 1px solid #fff;
  }

  .image-annotator {
    display: flex;
    height: 100%;
  }

  .image-container {
    flex: 1;
    position: relative;
    overflow: auto;
  }

  .image-container img {
    max-width: 100%;
    height: auto;
  }

  .annotation {
    position: absolute;

    box-sizing: border-box;
  }

  canvas {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    pointer-events: none; /* 防止遮挡鼠标事件 */
  }
</style>

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

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

相关文章

【办公利器】ReNamer (批量文件重命名工具) Pro v7.6.0.4 多语便携版,海量文件秒速精准改名!

ReNamer是一款功能强大的文件重命名工具&#xff0c;它可以帮助用户快速方便地批量重命名文件和文件夹。 软件功能 批量重命名&#xff1a;ReNamer可以同时处理多个文件和文件夹&#xff0c;并对其进行批量重命名&#xff0c;从而节省时间和劳动力。灵活的重命名规则&#xff…

unity学习13:gameobject的组件component以及tag, layer 归类

目录 1 gameobject component 是unity的基础 1.1 类比 1.2 为什么要这么设计&#xff1f; 2 从空物体开始 2.1 创建2个物体 2.2 给 empty gameobject添加组件 3 各种组件和新建组件 3.1 点击 add component可以添加各种组件 3.2 新建组件 3.3 组件的操作 3.4 特别的…

数据库模型全解析:从文档存储到搜索引擎

目录 前言1. 文档存储&#xff08;Document Store&#xff09;1.1 概念与特点1.2 典型应用1.3 代表性数据库 2. 图数据库&#xff08;Graph DBMS&#xff09;2.1 概念与特点2.2 典型应用2.3 代表性数据库 3. 原生 XML 数据库&#xff08;Native XML DBMS&#xff09;3.1 概念与…

CSS——1.优缺点

<!DOCTYPE html> <html><head><meta charset"UTF-8"><title></title><link rel"stylesheet" type"text/css" href"1-02.css"/></head><body><!--css&#xff1a;层叠样式表…

UE5本地化和国际化语言

翻译语言 工具 - 本地化控制板 Localization Dashboard 修改图中这几个地方就可以 点击箭头处&#xff0c;把中文翻译成英语&#xff0c;如果要更多语言就点 添加新语言 最后点击编译即可 编译完&#xff0c;会在目录生成文件夹 设置界面相关蓝图中设置 切换本地化语言 必须在…

python学习笔记—15—数据容器之列表

1. 数据容器 列表(list)、元组(tuple)、字符串(str)、集合(set)、字典(dict) 2. 列表 (1) 定义 tmp_list ["super", "carry", "doinb"] print(f"tmp_list {tmp_list}, tmp_list type is {type(tmp_list)}") tmp_list1 ["doi…

【简博士统计学习方法】第1章:4. 模型的评估与选择

4. 模型的评估与选择 4.1 训练误差与测试误差 假如存在样本容量为 N N N的训练集&#xff0c;将训练集送入学习系统可以训练学习得到一个模型&#xff0c;我们将这么模型用决策函数的形式表达&#xff0c;也就是 y f ^ ( x ) y\hat{f}(x) yf^​(x)&#xff0c;关于模型的拟合…

Unity自定义编辑器:基于枚举类型动态显示属性

1.参考链接 2.应用 target并设置多选编辑 添加[CanEditMultipleObjects] using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEditor;[CustomEditor(typeof(LightsState))] [CanEditMultipleObjects] public class TestInspector :…

cesium小知识:3D tiles 概述、特点、示例

Cesium 的 3D Tiles 是一种高效的、流式传输的三维地理空间数据格式,专为在Web浏览器中快速渲染大规模三维场景而设计。3D Tiles 支持多种几何类型,包括点云、多边形、模型等,并且可以包含丰富的属性信息和层次细节(LOD, Level of Detail)结构,以确保不同设备和网络条件下…

【微服务】7、分布式事务

在分布系统中&#xff0c;一个业务由多个服务合作完成&#xff0c;每个服务有自己的事务&#xff0c;多个事务需同时成功或失败&#xff0c;这样的事务称为分布式事务。 其中每个服务的事务叫分支事务&#xff0c;整个业务的统一事务叫全局事务。 分布式事务相关知识讲解 课程引…

基于 Boost.Asio 和 Boost.Beast 的异步 HTTP 服务器(学习记录)

已完成功能&#xff1a; 支持 GET 和 POST 请求的路由与回调处理。 解析URL请求。 单例模式 管理核心业务逻辑。 异步 I/O 技术和 定时器 控制超时。 通过回调函数注册机制&#xff0c;可以灵活地为不同的 URL 路由注册处理函数。 1. 项目背景 1.1 项目简介 本项目是一个基于…

Linux标准IOday1

1:思维导图 2:将 student.c这个练习题&#xff0c;改成链表后实现 头文件link.h #ifndef __STRUCT_H__ #define __STRUCT_H__ #include <stdio.h> #include <stdlib.h> typedef struct Student{char name[20];double math;double chinese;double english;double…

全局变量(PHP)(小迪网络安全笔记~

免责声明&#xff1a;本文章仅用于交流学习&#xff0c;因文章内容而产生的任何违法&未授权行为&#xff0c;与文章作者无关&#xff01;&#xff01;&#xff01; 附&#xff1a;完整笔记目录~ ps&#xff1a;本人小白&#xff0c;笔记均在个人理解基础上整理&#xff0c;…

gateway的路径匹配介绍

gateway是一个单独服务。通过网关端口和predicates进行匹配服务 1先看配置。看我注解你就明白了。其实就是/order/**配置机制直接匹配到orderservice服务。 2我试着请求一个路径&#xff0c;请求成功。下面第三步是请求的接口。 3接口。

RabbitMQ-基本使用

RabbitMQ: One broker to queue them all | RabbitMQ 官方 安装到Docker中 docker run \-e RABBITMQ_DEFAULT_USERrabbit \-e RABBITMQ_DEFAULT_PASSrabbit \-v mq-plugins:/plugins \--name mq \--hostname mq \-p 15672:15672 \-p 5672:5672 \--network mynet\-d \rabbitmq:3…

模式识别-Ch2-分类错误率

分类错误率 最小错误率贝叶斯决策 样本 x x x的错误率&#xff1a; 任一决策都可能会有错误。 P ( error ∣ x ) { P ( w 2 ∣ x ) , if we decide x as w 1 P ( w 1 ∣ x ) , if we decide x as w 2 P(\text{error}|\mathbf{x})\begin{cases} P(w_2|\mathbf{x}), &…

CAD批量打印可检索的PDF文件

本文虽介绍CAD使用方法&#xff0c;但还是劝告大家尽早放弃使用CAD软件。。。。太TM难用了 当你打开CAD时发现如下一堆图纸&#xff0c;但是不想一个一个打印时。你可以按照下面操作实现自动识别图框实现批量打印。 1.安装批量打印插件 2.安装后打开CAD&#xff0c;输入命令Bp…

BERT:深度双向Transformer的预训练用于语言理解

摘要 我们介绍了一种新的语言表示模型&#xff0c;名为BERT&#xff0c;全称为来自Transformer的双向编码器表示。与最近的语言表示模型&#xff08;Peters等&#xff0c;2018a&#xff1b;Radford等&#xff0c;2018&#xff09;不同&#xff0c;BERT旨在通过在所有层中联合调…

搭建企业AI助理的创新应用与案例分析

在大健康零售行业&#xff0c;企业面临着日益增长的市场需求和复杂的供应链管理挑战。AI助理的应用不仅能够提升客户服务效率&#xff0c;还能优化供应链管理&#xff0c;降低运营成本。 一、AI助理在大健康零售行业的创新应用 个性化健康咨询 AI助理可以通过分析客户的健康…

apex安装

安装过程复杂曲折&#xff0c;网上说的很多办法&#xff0c;貌似成功了&#xff0c;实际还是没起作用。 先说成功过程&#xff0c;执行下面命令&#xff0c;安装成功&#xff08;当然&#xff0c;前提是你要先配置好编译环境&#xff09;&#xff1a; &#xff08;我的环境&a…