当前位置: 首页 > article >正文

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


http://www.kler.cn/a/394068.html

相关文章:

  • 408模拟卷较难题(无分类)
  • Ubuntu 的 ROS 操作系统安装与测试
  • Elasticsearch 8.16:适用于生产的混合对话搜索和创新的向量数据量化,其性能优于乘积量化 (PQ)
  • neo4j desktop基本入门
  • 建筑施工特种作业人员安全生产知识试题
  • 【STM32F1】——无线收发模块RF200与串口通信
  • Jmeter中的配置原件(五)
  • 微服务电商平台课程四: 搭建本地前端服务
  • WPF学习之路,控件的只读、是否可以、是否可见属性控制
  • 〔 MySQL 〕数据类型
  • 基于HTTP编写ping操作
  • Day44 | 动态规划 :状态机DP 买卖股票的最佳时机IV买卖股票的最佳时机III
  • 【大数据学习 | HBASE高级】rowkey的设计,hbase的预分区和压缩
  • redis 原理篇 31 redis内存回收 内存淘汰策略
  • 【混沌系统】洛伦兹吸引子-Python动画
  • vueRouter路由切换时实现页面子元素动画效果, 左右两侧滑入滑出效果
  • 数据分析编程:SQL,Python or SPL?
  • 机器学习—为什么我们需要激活函数
  • 分享 | 中望3D 2025发布会提及的工业数字化MBD是什么?
  • 本溪与深圳市新零售产业互联协会共商世界酒中国菜湾区农业发展
  • 力扣257:二叉树的所有路径
  • adb不识别设备(手机)的若干情形及解决方法
  • 研究生如何远控实验室电脑?远程办公功能使用教程
  • 论文学习_Efficient Algorithms for Personalized PageRank Computation: A Survey
  • 【案例】定性数据分析软件NVivo 在医疗保健领域的应用
  • A034-基于Spring Boot的供应商管理系统的设计与实现