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

效果:

页面描述:

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

实现思路:

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><Paginationv-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.rhkb.cn/news/505460.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Elasticsearch—索引库操作(增删查改)

Elasticsearch中Index就相当于MySQL中的数据库表 Mapping映射就类似表的结构。 因此我们想要向Elasticsearch中存储数据,必须先创建Index和Mapping 1. Mapping映射属性 Mapping是对索引库中文档的约束&#xff0c;常见的Mapping属性包括&#xff1a; type&#xff1a;字段数据类…

在Jmeter中跨线程组传递变量(token)--设置全局变量

参考资料&#xff1a; Jmeter跨线程组传递参数(token)_jmeter获取token传递给下一个线程组详解-CSDN博客 最近工作中遇到一个问题&#xff0c;就是如何跨线程组传递变量&#xff0c;比如token,后来找到一些资料解决了该问题&#xff0c;目前有两种方式都可以解决&#xff0c;我…

【C++】揭开C++类与对象的神秘面纱(首卷)(类的基础操作详解、实例化艺术及this指针的深究)

文章目录 一、类的定义1.类定义格式2.类访问限定符3.类域 二、类的实例化1.实例化概念2.对象的大小 三、隐藏的this指针与相关练习1.this指针的引入与介绍练习1练习2练习3 一、类的定义 1.类定义格式 在讲解类的作用之前&#xff0c;我们来看看类是如何定义的&#xff0c;在C中…

前端JavaScript中some方法的运用

一&#xff0e;前言 在我们的日常工作中&#xff0c;有时候仅仅需要找到某个数组中的值&#xff0c;就可以返还结果的话&#xff0c;笔者建议就可以使用some方法&#xff0c;这比遍历整个数组高效一些。 二&#xff0e;应用 首先&#xff0c;看官方定义&#xff1a;JavaScri…

安装vue脚手架出现的一系列问题

安装vue脚手架出现的一系列问题 前言使用 npm 安装 vue/cli2.权限问题及解决方法一&#xff1a;可以使用管理员权限进行安装。方法二&#xff1a;更改npm全局安装路径 前言 由于已有较长时间未进行 vue 项目开发&#xff0c;今日着手准备开发一个新的 vue 项目时&#xff0c;在…

基于Python实现的通用小规模搜索引擎

基于Python实现的通用小规模搜索引擎 1.项目简介 1.1背景 《信息内容安全》网络信息内容获取技术课程项目设计 一个至少能支持10个以上网站的爬虫程序&#xff0c;且支持增量式数据采集;并至少采集10000个实际网页;针对采集回来的网页内容&#xff0c; 能够实现网页文本的分…

鸿蒙面试 2025-01-10

写了鉴权工具&#xff0c;你在项目中申请了那些权限&#xff1f;&#xff08;常用权限&#xff09; 位置权限 &#xff1a; ohos.permission.LOCATION_IN_BACKGROUND&#xff1a;允许应用在后台访问位置信息。 ohos.permission.LOCATION&#xff1a;允许应用访问精确的位置信息…

【硬件测试】基于FPGA的BPSK+帧同步系统开发与硬件片内测试,包含高斯信道,误码统计,可设置SNR

目录 1.硬件片内测试效果 2.算法涉及理论知识概要 2.1 bpsk 2.2 帧同步 3.Verilog核心程序 4.开发板使用说明和如何移植不同的开发板 5.完整算法代码文件获得 1.硬件片内测试效果 本文是之前写的文章 《基于FPGA的BPSK帧同步系统verilog开发,包含testbench,高斯信道,误…

MySQL 视图 存储过程与存储函数

第十四章_视图、第十五章 _存储过程与存储函数 1.常见的数据库对象 1. 表&#xff08;Table&#xff09; 用于存储结构化数据的基本对象&#xff0c;由行&#xff08;记录&#xff09;和列&#xff08;字段&#xff09;组成。 2. 视图&#xff08;View&#xff09; 基于一…

Chrome_60.0.3112.113_x64 单文件版 下载

单文件&#xff0c;免安装&#xff0c;直接用~ Google Chrome, 免費下載. Google Chrome 60.0.3112.113: Chrome 是 Google 開發的網路瀏覽器。它的特點是速度快,功能多。 下载地址: https://blog.s3.sh.cn/thread-150-1-1.htmlhttps://blog.s3.sh.cn/thread-150-1-1.html

CTFshow—文件包含

Web78-81 Web78 这题是最基础的文件包含&#xff0c;直接?fileflag.php是不行的&#xff0c;不知道为啥&#xff0c;直接用下面我们之前在命令执行讲过的payload即可。 ?filephp://filter/readconvert.base64-encode/resourceflag.php Web79 这题是过滤了php&#xff0c;…

python学opencv|读取图像(二十九)使用cv2.getRotationMatrix2D()函数旋转缩放图像

【1】引言 前序已经学习了如何平移图像&#xff0c;相关文章链接为&#xff1a; python学opencv|读取图像&#xff08;二十七&#xff09;使用cv2.warpAffine&#xff08;&#xff09;函数平移图像-CSDN博客 在此基础上&#xff0c;我们尝试旋转图像的同时缩放图像。 【2】…

24下半年软考「单独划线」合格标准已公布!

2024年下半年计算机技术与软件专业技术资格考试单独划线地区合格标准已公布&#xff01; 其中初级和中级单独划线地区合格标准各科目均为39分&#xff0c;高级各科目为40分&#xff0c;符合单独划线地区的同学可以去申请证书了。 一、证书效力 在单独划线地区报名参加相关职业…

Linux第一课:c语言 学习记录day06

四、数组 冒泡排序 两两比较&#xff0c;第 j 个和 j1 个比较 int a[5] {5, 4, 3, 2, 1}; 第一轮&#xff1a;i 0 n&#xff1a;n个数&#xff0c;比较 n-1-i 次 4 5 3 2 1 // 第一次比较 j 0 4 3 5 2 1 // 第二次比较 j 1 4 3 2 5 1 // 第三次比较 j 2 4 3 2 1 5 // …

前端用json-server来Mock后端返回的数据处理

<html><body><div class"login-container"><h2>登录</h2><div class"login-form"><div class"form-group"><input type"text" id"username" placeholder"请输入用户名&q…

是德科技M9010A PXIe 机箱+M9037A模块,台式应用的理想之选

Keysigh是德科技M9010A PXIe 机箱M9037A模块 M9010A PXIe 10 插槽 Gen 3 机箱提供***的灵活性、兼容性和性能&#xff0c;而且外形更小巧&#xff0c;是台式应用的理想之选。它拥有 8 个 PXIe 混合插槽&#xff0c;使系统设计人员能够更灵活地混合和搭配 PXIe 和混合兼容模块的…

【算法刷题】leetcode hot 100 滑动窗口

文章目录 3. 无重复字符的最长子串438. 找到字符串中所有字母异位词总结 3. 无重复字符的最长子串 leetcode&#xff1a;https://leetcode.cn/problems/longest-substring-without-repeating-characters/?envTypestudy-plan-v2&envIdtop-100-liked 滑动窗口 &#xff0…

企业级PHP异步RabbitMQ协程版客户端 2.0 正式发布

概述 workerman/rabbitmq 是一个异步RabbitMQ客户端&#xff0c;使用AMQP协议。 RabbitMQ是一个基于AMQP&#xff08;高级消息队列协议&#xff09;实现的开源消息组件&#xff0c;它主要用于在分布式系统中存储和转发消息。RabbitMQ由高性能、高可用以及高扩展性出名的Erlan…

基于SpringBoot的洗浴管理系统

作者&#xff1a;计算机学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等&#xff0c;“文末源码”。 专栏推荐&#xff1a;前后端分离项目源码、SpringBoot项目源码、Vue项目源码、SSM项目源码、微信小程序源码 精品专栏&#xff1a;…

直流无刷电机控制(FOC):电流模式

目录 概述 1 系统框架结构 1.1 硬件模块介绍 1.2 硬件实物图 1.3 引脚接口定义 2 代码实现 2.1 软件架构 2.2 电流检测函数 3 电流环功能实现 3.1 代码实现 3.2 测试代码实现 4 测试 概述 本文主要介绍基于DengFOC的库函数&#xff0c;实现直流无刷电机控制&#x…