Vue Canvas实现区域拉框选择

news2024/11/19 8:35:45

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;
    },
  },

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

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

相关文章

实例教程:BBDB为AHRS算法开发提供完善的支撑环境(上)

1. 概述 本教程将结合程序代码及CSS控制站工程&#xff0c;讲述如何基于PH47代码框架的BBDB固件版本&#xff0c;为开发自己的AHRS姿态解算算法提供完善支撑环境&#xff0c;以及数据分析手段。 BBDB固件已内置了一套姿态解算算法。对于需要进行AHRS算法开发研究的开发者&…

Linux操作系统 ----- (5.系统管理)

目录 1.总结 2.本章学习目标 3.图形界面管理 3.1.X-Window图形界面概述 3.2.X-Window的结构 3.3.X-Window的特点 3.4.UKUI图形环境 3.5.桌面 3.5.1.桌面图标 3.5.2.计算机属性 3.5.3.桌面快捷菜单 3.6.任务栏 3.6.1.开始菜单 3.6.2.显示任务视图 3.6.3.文件管理器…

hive复杂数据类型Array Map Struct 炸裂函数explode

1、Array的使用 create table tableName( ...... colName array<基本类型> ...... ) 说明&#xff1a;下标从0开始&#xff0c;越界不报错&#xff0c;以null代替 arr1.txtzhangsan 78,89,92,96 lisi 67,75,83,94 王五 23,12 新建表&#xff1a; create table arr1(n…

基于Python实现的HDR图像处理算法

此代码会读取两张图片&#xff0c;一张用于保留高光细节&#xff0c;另一张用于保留暗部细节。两张图片按指定比例进行像素融合&#xff0c;最终生成一张合成的HDR图片。 import cv2 import numpy as npdef hdr_fusion(highlight_img_path, shadow_img_path, output_path, alp…

网络协议(4)拥塞控制

之前已经说过了tcp也是会考虑网络的情况的&#xff0c;也就是当网络出现问题的时候tcp不会再对报文进行重传。当所有的用户在网络不好的时候都不会对丢失的报文进行重传。这样就会防止网络瘫痪。 这样的机制也就是tcp会进行拥塞控制。 拥塞控制 所谓的慢启动看下面这张图就能…

解决 IDEA 修改代码重启不生效的问题

前言 在使用 IntelliJ IDEA 进行 Java 项目开发时&#xff0c;有时会遇到一个令人头疼的问题&#xff1a;修改了代码后&#xff0c;重启服务却发现更改没有生效。通常情况下&#xff0c;解决这个问题需要通过 Maven 的 clean 和 compile 命令来强制重新编译&#xff0c;但这显…

git使用及上线流程(仅为我工作中常用)

推荐软件或者直接终端 ⚠️注意&#xff1a;在确保远程和本地分支都可使用的情况下 git常见使用命令 ls---查看所有目录 pwd---本机密码 cd 目录名---进入目录 Touch ---创建文本文件 git status---查看状态 git branch---查看分支 git pull---拉取远程最新代码 git checkou…

12.C++内存管理1(C/C++内存分布,C语言动态内存管理)

⭐本篇重点&#xff1a;C/C内存分布&#xff0c;C语言动态内存管理 ⭐本篇代码&#xff1a;c学习/04.c-动态内存管理 橘子真甜/c-learning-of-yzc - 码云 - 开源中国 (gitee.com) 目录 一. C/C内存分布&#xff08;C/C内存地址空间&#xff09; 二. C语言动态内存管理 2.1 …

游戏引擎学习第15天

视频参考:https://www.bilibili.com/video/BV1mbUBY7E24 关于游戏中文件输入输出&#xff08;IO&#xff09;操作的讨论。主要分为两类&#xff1a; 只读资产的加载 这部分主要涉及游戏中用于展示和运行的只读资源&#xff0c;例如音乐、音效、美术资源&#xff08;如 3D 模型和…

JavaWeb——JS、Vue

目录 1.JavaScript a.概述 b.引入方式 c.JS的基础语法 d.JS函数 e.JS对象 f.JS事件监听 2.Vue a.概述 b.Vue常用指令 d.生命周期 1.JavaScript a.概述 JavaScript是一门跨平台、面向对象的脚本语言。是用来控制网页行为的&#xff0c;它能使网页可交互。JavaScript和…

HarmonyOs鸿蒙开发实战(16)=>沉浸式效果第一种方案一窗口全屏布局方案

1.沉浸式效果的目的 开发应用沉浸式效果主要指通过调整状态栏、应用界面和导航条的显示效果来减少状态栏导航条等系统界面的突兀感&#xff0c;从而使用户获得最佳的UI体验。 2.窗口全屏布局方案介绍 调整布局系统为全屏布局&#xff0c;界面元素延伸到状态栏和导航条区域实现沉…

spi 回环

///tx 极性0 &#xff08;sclk信号线空闲时为低电平&#xff09; /// 相位0 (在sclk信号线第一个跳变沿进行采样) timescale 1ns / 1ps//两个从机 8d01 8d02 module top(input clk ,input rst_n,input [7:0] addr ,input …

CF862B Mahmoud and Ehab and the bipartiteness(二分图的性质)

思路&#xff1a;一个二分图是由两个集合组成的&#xff0c;同一个集合中的节点间不能连边&#xff0c;所以一个二分图最多有cnt[1]*cnt[2]条边&#xff0c;题目给出一个树的n-1条边&#xff0c;要我们添加最多的边数使他成为二分图&#xff0c;添加的边数就是cnt[1]*cnt[2]-n1…

docker:基于Dockerfile镜像制作完整案例

目录 摘要目录结构介绍起始目录package目录target目录sh目录init.sh脚本start.sh脚本stop.sh脚本restart.sh脚本 config目录 步骤1、编写dockerfilescript.sh脚本 2、构件镜像查看镜像 3、保存镜像到本地服务器4、复制镜像文件到指定目录&#xff0c;并执行init.sh脚本5、查看挂…

Redis自学之路—基础数据结构具体方法解析(五)

目录 简介 数据结果具体方法解析 字符串(String) 操作命令 set设置值 setex setnx get获取值 del删除key mset批量设置值 incr数字运算 append追加指令 strlen字符串长度 getset设置并返回原值 setrange设置指定位置的字符 getrange截取字符串 命令的时间复杂…

通过华为鲲鹏认证发行上市的集成平台产品推荐

华为鲲鹏认证是技术实力与品质的权威象征&#xff0c;代表着产品达到了高标准的要求。从技术层面看&#xff0c;认证确保产品与华为鲲鹏架构深度融合&#xff0c;能充分释放鲲鹏芯片的高性能、低功耗优势&#xff0c;为集成平台的高效运行提供强大动力。在安全方面&#xff0c;…

使用 AMD GPU 实现 Segment Anything

Segment Anything with AMD GPUs — ROCm Blogs 作者&#xff1a; Sean Song 发布日期&#xff1a;2024年6月4日 介绍 分割任务——识别图像中哪些像素属于某对象——是计算机视觉中的一个基础任务&#xff0c;应用广泛&#xff0c;从科学图像分析到照片编辑。Segment Anyth…

Spring Cloud Stream实现数据流处理

1.什么是Spring Cloud Stream&#xff1f; 我看很多回答都是“为了屏蔽消息队列的差异&#xff0c;使我们在使用消息队列的时候能够用统一的一套API&#xff0c;无需关心具体的消息队列实现”。 这样理解是有些不全面的&#xff0c;Spring Cloud Stream的核心是Stream&#xf…

无人机飞手入门指南

无人机飞手入门指南旨在为初学者提供一份全面的学习路径和实践建议&#xff0c;帮助新手快速掌握无人机飞行技能并了解相关法规知识。以下是一份详细的入门指南&#xff1a; 一、了解无人机基础知识 1. 无人机构造&#xff1a;了解无人机的组成部分&#xff0c;如机身、螺旋桨…

使用Mac下载MySQL修改密码

Mac下载MySQL MySQL官网链接MySQL​​​​​​ 当进入到官网后下滑到community社区&#xff0c;进行下载 然后选择community sever下载 这里就是要下载的界面&#xff0c;如果需要下载之前版本的话可以点击archives&#xff0c; 可能会因为这是外网原因&#xff0c;有时候下…