Vue Canvas实现区域拉框选择

canvas.vue组件

<template>
    <div class="all" ref="divideBox">
        <!-- 显示图片,如果 imgUrl 存在则显示 -->
        <img id="img" v-if="imgUrl" :src="imgUrl" oncontextmenu="return false" draggable="false">
        <!-- 画布元素,绑定鼠标事件 -->
        <canvas ref="canvas" id="mycanvas" @mousedown="startDraw" @mousemove="onMouseMove" @mouseup="endDraw"
            @click="onClick" :width="canvasWidth" :height="canvasHeight" oncontextmenu="return false"
            draggable="false"></canvas>
        <el-dialog title="编辑区域数据" :visible.sync="dialogVisible" width="500">
            <div class="dialogDiv">
                <el-form :model="form" ref="form" label-width="110px" :rules="rules">
                    <el-form-item label="车辆类型" prop="type">
                        <el-select style="width: 100%;" v-model="form.type" placeholder="请选择车辆类型" size="small"
                            clearable>
                            <el-option v-for="item in carTypeList" :key="item.value" :label="item.label"
                                :value="item.value" />
                        </el-select>
                    </el-form-item>
                    <el-form-item label="JSON数据" prop="jsonData">
                        <el-input size="small" type="textarea" v-model="form.jsonData" rows="10"></el-input>
                    </el-form-item>
                </el-form>
            </div>
            <span slot="footer" class="dialog-footer">
                <el-button type="danger" @click="del">删 除</el-button>
                <el-button type="primary" @click="clickOk">确 定</el-button>
            </span>
        </el-dialog>
    </div>
</template>

<script>
export default {
    name: 'CanvasBox',
    // 引入组件才能使用
    props: {
        // 画布宽度
        canvasWidth: {
            type: Number,
            default: 0
        },
        // 画布高度
        canvasHeight: {
            type: Number,
            default: 0
        },
        // 时间戳
        timeStamp: {
            type: Number,
            default: 0
        },
        // 图片 URL
        imgUrl: {
            type: String,
            default: ""
        },
        // 类型颜色
        type: {
            type: String,
            default: ""
        },
    },
    components: {},
    data() {
        return {
            rules: {
                type: [
                    { required: true, message: '车辆类型不能为空', trigger: ['change', 'blur'] }
                ],
                jsonData: [
                    { required: true, message: 'JSON数据不能为空', trigger: ['change', 'blur'] }
                ],
            },
            carTypeList: [
                {
                    value: "1",
                    label: "人员"
                },
                {
                    value: "2",
                    label: "车辆"
                }
            ],
            // 表单值
            form: {
                id: null,
                type: '',
                jsonData: ''
            },
            dialogVisible: false,
            originalCanvasWidth: this.canvasWidth,
            originalCanvasHeight: this.canvasHeight,
            url: null,
            // 是否是绘制当前的草图框
            isDrawing: false,
            start: { x: 0, y: 0 },
            end: { x: 0, y: 0 },
            // 储存所有的框数据
            boxes: [],
            // 框文字
            selectedCategory: {
                modelName: ""
            },
            categories: [],
            image: null, // 用于存储图片
            imageWidth: null, // 图片初始宽度
            imageHeight: null, // 图片初始高度
            piceList: [],
            startTime: null, // 用于记录鼠标按下的时间
            categoryColors: {
                '车辆': 'red',
                '人员': 'yellow'
            },
        };
    },
    watch: {
        // 清空画布
        timeStamp() {
            this.test();
        },
        // 监听画布宽度
        canvasWidth(newVal) {
            this.$nextTick(() => {
                this.adjustBoxesOnResize();
                this.draw();
            })
        },
        // 监听类型
        type(newVal) {
            this.selectedCategory.modelName = newVal === '1' ? '人员' : newVal === '2' ? '车辆' : ''
        }
    },
    mounted() {
        this.draw();
        // 添加鼠标进入和离开画布的事件监听
        this.$refs.canvas.addEventListener('mouseenter', this.onMouseEnter);
        this.$refs.canvas.addEventListener('mouseleave', this.onMouseLeave);
    },
    beforeDestroy() {
        // 移除事件监听器
        this.$refs.canvas.removeEventListener('mouseenter', this.onMouseEnter);
        this.$refs.canvas.removeEventListener('mouseleave', this.onMouseLeave);
    },
    methods: {
        // 清空画布
        test() {
            this.boxes = []
            this.$nextTick(() => {
                this.draw();
            })
        },
        // 删除区域
        del() {
            if (this.form.id !== null) {
                this.boxes = this.boxes.filter(box => box.id !== this.form.id); // 根据ID删除多边形
                // this.form.id = null; // 清空ID 
                // 清空form
                this.form = {
                    id: null,
                    type: '',
                    jsonData: ''
                };
                this.dialogVisible = false;
                this.$nextTick(() => {
                    this.adjustBoxesOnResize();
                    this.draw();
                })
            }
        },
        // 确认
        clickOk() {
            this.$refs.form.validate((valid) => {
                if (valid) {
                    if (this.form.id !== null) {
                        const boxIndex = this.boxes.findIndex(box => box.id === this.form.id);
                        if (boxIndex !== -1) {
                            const newCategory = this.form.type === '1' ? '人员' : '2' ? '车辆' : '';
                            this.boxes[boxIndex] = {
                                ...this.boxes[boxIndex],
                                category: newCategory,
                                jsonData: this.form.jsonData
                            };
                        }
                    }
                    this.dialogVisible = false;
                    this.draw();
                }
            });
        },
        // 点击框框
        onClick(event) {
            const rect = this.$refs.canvas.getBoundingClientRect();
            const mouseX = event.clientX - rect.left;
            const mouseY = event.clientY - rect.top;
            for (let box of this.boxes) {
                if (mouseX >= box.start.x && mouseX <= box.end.x &&
                    mouseY >= box.start.y && mouseY <= box.end.y) {
                    // console.log("点击的多边形参数", box);
                    let jsons = box.category === '人员' ? `{\n"id": 0,\n"lifeJacket": true,\n"raincoat": false,\n"reflectiveVest": false,\n"safetyHat": { "color": "red" },\n"type": "rectangle",\n"workingClothes": false\n}` : `{\n"carType": "forklift",\n"hasGoods": true,\n"id": 0,\n"speed": 100,\n"type": "rectangle"\n}`
                    this.form = {
                        id: box.id, // 保存当前选中的多边形ID
                        type: box.category === '人员' ? '1' : '2',
                        jsonData: box.jsonData || jsons,
                    };
                    this.dialogVisible = true;
                    break;
                }
            }
        },
        // 新增的方法
        onMouseEnter() {
            // 当鼠标进入画布时,初始化光标样式为默认
            this.$refs.canvas.style.cursor = 'default';
        },
        // 当鼠标离开画布时,确保光标样式为默认
        onMouseLeave() {
            this.$refs.canvas.style.cursor = 'default';
        },
        adjustBoxesOnResize() {
            if (this.originalCanvasWidth === 0 || this.originalCanvasHeight === 0) return;
            const scaleX = this.canvasWidth / this.originalCanvasWidth;
            const scaleY = this.canvasHeight / this.originalCanvasHeight;
            this.boxes = this.boxes.map(box => ({
                id: box.id,
                category: box.category,
                start: {
                    x: box.start.x * scaleX,
                    y: box.start.y * scaleY
                },
                end: {
                    x: box.end.x * scaleX,
                    y: box.end.y * scaleY
                },
                jsonData: box.jsonData,
            }));
            this.originalCanvasWidth = this.canvasWidth;
            this.originalCanvasHeight = this.canvasHeight;
        },
        // 开始绘制
        startDraw(event) {
            if (event.which !== 1) return;
            if (!this.type) {
                this.$message({
                    message: '请先选择车辆类型',
                    type: 'warning'
                });
                return;
            }
            this.isDrawing = true;
            const rect = this.$refs.canvas.getBoundingClientRect();
            const scaleX = this.canvasWidth / this.originalCanvasWidth;
            const scaleY = this.canvasHeight / this.originalCanvasHeight;
            this.start = {
                x: (event.clientX - rect.left) / scaleX,
                y: (event.clientY - rect.top) / scaleY
            };
            // 记录鼠标按下的时间
            this.startTime = Date.now();
        },
        // 鼠标移动时更新绘制终点并重绘
        onMouseMove(event) {
            if (!this.isDrawing) {
                const rect = this.$refs.canvas.getBoundingClientRect();
                const mouseX = event.clientX - rect.left;
                const mouseY = event.clientY - rect.top;
                let cursorStyle = 'default';
                // 检查鼠标是否在任何框内
                for (let box of this.boxes) {
                    if (mouseX >= box.start.x && mouseX <= box.end.x &&
                        mouseY >= box.start.y && mouseY <= box.end.y) {
                        cursorStyle = 'pointer';
                        break; // 找到一个匹配的框后停止搜索
                    }
                }
                // 更新光标样式
                this.$refs.canvas.style.cursor = cursorStyle;
            }
            // 继续原有逻辑
            if (!this.isDrawing) return;
            const rect = this.$refs.canvas.getBoundingClientRect();
            const scaleX = this.canvasWidth / this.originalCanvasWidth;
            const scaleY = this.canvasHeight / this.originalCanvasHeight;
            this.end = {
                x: (event.clientX - rect.left) / scaleX,
                y: (event.clientY - rect.top) / scaleY
            };
            this.draw();
        },
        // 结束绘制
        endDraw(event) {
            if (!this.type) return;
            this.isDrawing = false;
            const endTime = Date.now(); // 获取鼠标释放的时间
            const timeDifference = endTime - this.startTime; // 计算时间差
            // 如果时间差小于 100 毫秒,则认为用户只是点击了一下
            if (timeDifference < 200) {
                return;
            }
            const distanceThreshold = 5; // 定义一个最小距离阈值
            const distance = Math.sqrt(
                Math.pow((this.end.x - this.start.x), 2) +
                Math.pow((this.end.y - this.start.y), 2)
            );
            // 只有当距离大于阈值时才绘制框
            if (distance > distanceThreshold) {
                const boxId = Date.now(); // 生成唯一的时间戳ID
                this.boxes.push({
                    id: boxId, // 添加唯一ID
                    start: this.start,
                    end: this.end,
                    category: this.selectedCategory.modelName,
                    jsonData: '' // 初始JSON数据为空
                });
                this.draw();
            }
        },
        // 删除选中的框
        deleteSelectedBoxes() {
            this.boxes = this.boxes.filter(box => box.category !== this.selectedCategory.modelName);
            this.draw();
        },
        // 绘制方法
        draw() {
            const canvas = this.$refs.canvas;
            const context = canvas.getContext('2d');
            context.clearRect(0, 0, canvas.width, canvas.height);
            if (this.boxes.length > 0) {
                // 绘制所有的框
                this.boxes.forEach(box => {
                    context.strokeStyle = this.categoryColors[box.category] || 'red'; // 默认为红色
                    context.strokeRect(box.start.x, box.start.y, box.end.x - box.start.x, box.end.y - box.start.y);
                    context.fillStyle = '#fff'; // 设置文字颜色为黑色
                    context.fillText(box.category, box.start.x, box.start.y - 5);
                });
            }
            // 绘制当前的草图框
            if (this.isDrawing) {
                const scaleX = this.canvasWidth / this.originalCanvasWidth;
                const scaleY = this.canvasHeight / this.originalCanvasHeight;
                context.strokeStyle = this.type === '2' ? 'red' : this.type === '1' ? 'yellow' : '#000000';
                context.strokeRect(
                    this.start.x * scaleX,
                    this.start.y * scaleY,
                    (this.end.x - this.start.x) * scaleX,
                    (this.end.y - this.start.y) * scaleY
                );
            }
            // console.log("所有框", this.boxes);
        },
    },
}
</script>

<style lang="scss" scoped>
.all {
    position: relative;
    width: 100%;
    height: 100%;

    .dialogDiv {
        width: 100%;
    }
}

#mycanvas {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    width: 100%;
    height: 100%;
}

#img {
    width: 100%;
    height: 100%;
    user-select: none;
}
</style>

父组件引入使用

 <CanvasBox ref="CanvasBox" v-if="canvasIsShow" :imgUrl="imgUrl" :type="form.type" :canvasWidth="canvasWidth" :canvasHeight="canvasHeight" :timeStamp="timeStamp" />

如果canvas是宽高不固定,可以改成响应式的
父组件中:

  mounted() {
    window.addEventListener('resize', this.onWindowResize);
    // 监听盒子尺寸变化
    // this.observeBoxWidth();
  },



  methods: {
    // 清空画布
    clearCanvas() {
      this.timeStamp = Date.now();
    },
    onWindowResize() {
      const offsetWidth = this.$refs.divideBox.offsetWidth;
      const offsetHeight = this.$refs.divideBox.offsetHeight;
      this.canvasWidth = offsetWidth
      this.canvasHeight = offsetHeight
      // console.log("canvas画布宽高", offsetWidth, offsetHeight);
    },
    // 保存
    async submitForm() {
      if (this.form.cameraId == null || this.form.cameraId == undefined) {
        this.$message({
          message: "请先选择摄像头",
          type: "warning",
        });
        return;
      }
      let newData = {
        "cameraId": this.form.cameraId,
        "photoCodeType": this.form.photoCodeType,
        "sendDataDtoList": [
          // {
          //   "type": 2,
          //   "pointList": [
          //     [
          //       544.45,
          //       432.42
          //     ],
          //     [
          //       595.19,
          //       455.17
          //     ]
          //   ],
          //   "jsonData": "{\"carType\":\"forklift\",\"hasGoods\":true,\"id\":0,\"speed\":100,\"type\":\"rectangle\"}"
          // }
        ]
      }
      // 现在盒子的宽高
      const offsetWidth = this.$refs.divideBox.offsetWidth
      const offsetHeight = this.$refs.divideBox.offsetWidth / this.pxData.x * this.pxData.y
      const boxesData = JSON.parse(JSON.stringify(this.$refs.CanvasBox.boxes))
      if (boxesData && boxesData.length > 0) {
        boxesData.forEach(item => {
          newData.sendDataDtoList.push({
            type: this.findValueByLabel(item.category),
            pointList: [
              [
                item.start.x / offsetWidth * this.pxData.x,
                item.start.y / offsetHeight * this.pxData.y,
              ],
              [
                item.end.x / offsetWidth * this.pxData.x,
                item.end.y / offsetHeight * this.pxData.y,
              ]
            ],
            jsonData: item.jsonData
          })
        })
      }
      console.log("发送车辆信息", newData);
      const { code } = await getRegionalTools(newData);
      if (code === 200) {
        this.$message({
          message: '发送成功',
          type: 'success'
        });
      }
    },
    findValueByLabel(label) {
      const item = this.carTypeList.find(item => item.label === label);
      return item ? item.value : null;
    },
  },

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

JavaWeb--MySQL

1. MySQL概述 首先来了解一下什么是数据库。 数据库&#xff1a;英文为 DataBase&#xff0c;简称DB&#xff0c;它是存储和管理数据的仓库。 像我们日常访问的电商网站京东&#xff0c;企业内部的管理系统OA、ERP、CRM这类的系统&#xff0c;以及大家每天都会刷的头条、抖音…

在MATLAB中导入TXT文件的若干方法

这是一篇关于如何在MATLAB中导入TXT文件的文章&#xff0c;包括示例代码和详细说明 文章目录 在MATLAB中导入TXT文件1. 使用readtable函数导入TXT文件示例代码说明 2. 使用load函数导入TXT文件示例代码说明 3. 使用importdata函数导入TXT文件示例代码说明 4. 自定义导入选项示例…

Clonezilla 再生龙制作系统U盘还原系统 ubuntu 22.04 server

参考 Clonezilla 再生龙制作系统U盘还原系统(UltraISO) https://blog.csdn.net/qq_57172130/article/details/120417522 Clonezilla-备份_部署ubuntu https://blog.csdn.net/xiaokai1999/article/details/131054826 基于再生龙&#xff08;clonezilla&#xff09;的Ubuntu镜…

号卡分销系统,号卡系统,物联网卡系统源码安装教程

号卡分销系统&#xff0c;号卡系统&#xff0c;物联网卡系统&#xff0c;&#xff0c;实现的高性能(PHP协程、PHP微服务)、高灵活性、前后端分离(后台)&#xff0c;PHP 持久化框架&#xff0c;助力管理系统敏捷开发&#xff0c;长期持续更新中。 主要特性 基于Auth验证的权限…

Nature Communications 基于触觉手套的深度学习驱动视触觉动态重建方案

在人形机器人操作领域&#xff0c;有一个极具价值的问题&#xff1a;鉴于操作数据在人形操作技能学习中的重要性&#xff0c;如何有效地从现实世界中获取操作数据的完整状态&#xff1f;如果可以&#xff0c;那考虑到人类庞大规模的人口和进行复杂操作的简单直观性与可扩展性&a…

STM32 独立看门狗(IWDG)详解

目录 一、引言 二、独立看门狗的作用 三、独立看门狗的工作原理 1.时钟源 2.计数器 3.喂狗操作 4.超时时间计算 5.复位机制 四、独立看门狗相关寄存器 1.键寄存器&#xff08;IWDG_KR&#xff09; 2.预分频寄存器&#xff08;IWDG_PR&#xff09; 3.重载寄存器&…

vue3点击按钮el-dialog对话框不显示问题

vue3弹框不显示问题&#xff0c;控制台也没报错 把 append-to-body:visible.sync"previewDialogOpen" 改为 append-to-bodyv-model"previewDialogOpen" 就好了。

vue项目使用eslint+prettier管理项目格式化

代码格式化、规范化说明 使用eslintprettier进行格式化&#xff0c;vscode中需要安装插件ESLint、Prettier - Code formatter&#xff0c;且格式化程序选择为后者&#xff08;vue文件、js文件要分别设置&#xff09; 对于eslint规则&#xff0c;在格式化时不会全部自动调整&…

Python爬虫----python爬虫基础

一、python爬虫基础-爬虫简介 1、现实生活中实际爬虫有哪些&#xff1f; 2、什么是网络爬虫&#xff1f; 3、什么是通用爬虫和聚焦爬虫&#xff1f; 4、为什么要用python写爬虫程序 5、环境和工具 二、python爬虫基础-http协议和chrome抓包工具 1、什么是http和https协议…

大数据新视界 -- 大数据大厂之 Impala 性能飞跃:动态分区调整的策略与方法(上)(21 / 30)

&#x1f496;&#x1f496;&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎你们来到 青云交的博客&#xff01;能与你们在此邂逅&#xff0c;我满心欢喜&#xff0c;深感无比荣幸。在这个瞬息万变的时代&#xff0c;我们每个人都在苦苦追寻一处能让心灵安然栖息的港湾。而 我的…

Java基础-Java多线程机制

(创作不易&#xff0c;感谢有你&#xff0c;你的支持&#xff0c;就是我前行的最大动力&#xff0c;如果看完对你有帮助&#xff0c;请留下您的足迹&#xff09; 目录 一、引言 二、多线程的基本概念 1. 线程与进程 2. 多线程与并发 3. 多线程的优势 三、Java多线程的实…

Unity中HDRP设置抗锯齿

一、以前抗锯齿的设置方式 【Edit】——>【Project Settings】——>【Quality】——>【Anti-aliasing】 二、HDRP项目中抗锯齿的设置方式 在Hierarchy中——>找到Camera对象——>在Inspector面板上——>【Camera组件】——>【Rendering】——>【Pos…

动手学深度学习72 优化算法

1. 优化算法 任意两点连线&#xff0c;所有线上的值都在集合里面–凸集 在机器学习&#xff0c;凹凸函数的区别&#xff1f; 凸函数表达能力有限 动量法&#xff1a; 比较平滑的改变方向&#xff0c;两个下降方向不一样【冲突】的时候&#xff0c;抵消掉一些使梯度的更新不那…

Linux:进程的优先级 进程切换

文章目录 前言一、进程优先级1.1 基本概念1.2 查看系统进程1.3 PRI和NI1.4 调整优先级1.4.1 top命令1.4.2 nice命令1.4.3 renice命令 二、进程切换2.1 补充概念2.2 进程的运行和切换步骤&#xff08;重要&#xff09; 二、Linux2.6内核进程O(1)调度队列&#xff08;重要&#x…

Python绘制雪花

文章目录 系列目录写在前面技术需求完整代码代码分析1. 代码初始化部分分析2. 雪花绘制核心逻辑分析3. 窗口保持部分分析4. 美学与几何特点总结 写在后面 系列目录 序号直达链接爱心系列1Python制作一个无法拒绝的表白界面2Python满屏飘字表白代码3Python无限弹窗满屏表白代码4…

2023年MathorCup数学建模B题城市轨道交通列车时刻表优化问题解题全过程文档加程序

2023年第十三届MathorCup高校数学建模挑战赛 B题 城市轨道交通列车时刻表优化问题 原题再现&#xff1a; 列车时刻表优化问题是轨道交通领域行车组织方式的经典问题之一。列车时刻表规定了列车在每个车站的到达和出发&#xff08;或通过&#xff09;时刻&#xff0c;其在实际…

AntFlow 0.11.0版发布,增加springboot starter模块,一款设计上借鉴钉钉工作流的免费企业级审批流平台

AntFlow 0.11.0版发布,增加springboot starter模块,一款设计上借鉴钉钉工作流的免费企业级审批流平台 传统老牌工作流引擎比如activiti,flowable或者camunda等虽然功能强大&#xff0c;也被企业广泛采用&#xff0c;然后也存着在诸如学习曲线陡峭&#xff0c;上手难度大&#x…

构建SSH僵尸网络

import argparse import paramiko# 定义一个名为Client的类&#xff0c;用于表示SSH客户端相关操作 class Client:# 类的初始化方法&#xff0c;接收主机地址、用户名和密码作为参数def __init__(self, host, user, password):self.host hostself.user userself.password pa…

小白快速上手 labelme:新手图像标注详解教程

前言 本教程主要面向初次使用 labelme 的新手&#xff0c;详细介绍了如何在 Windows 上通过 Anaconda 创建和配置环境&#xff0c;并使用 labelme 进行图像标注。 1. 准备工作 在开始本教程之前&#xff0c;确保已经安装了 Anaconda。可以参考我之前的教程了解 Anaconda 的下…

AB矩阵秩1乘法,列乘以行

1. AB矩阵相乘 2. 代码测试 python 代码 #!/usr/bin/env python # -*- coding:utf-8 -*- # FileName :ABTest.py # Time :2024/11/17 8:37 # Author :Jason Zhang import numpy as np from abc import ABCMeta, abstractmethodnp.set_printoptions(suppressTrue, pr…