Vue Canvas实现区域拉框选择

embedded/2024/11/19 3:34:06/

canvas.vue组件

javascript"><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},// 图片 URLimgUrl: {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 // 清空formthis.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, // 保存当前选中的多边形IDtype: 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(); // 生成唯一的时间戳IDthis.boxes.push({id: boxId, // 添加唯一IDstart: 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是宽高不固定,可以改成响应式的
父组件中:

javascript">  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 = offsetWidththis.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.offsetWidthconst offsetHeight = this.$refs.divideBox.offsetWidth / this.pxData.x * this.pxData.yconst 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.ppmy.cn/embedded/138680.html

相关文章

【Java豆瓣电影爬虫】——抓取电影详情和电影短评数据 -

一直想做个这样的爬虫&#xff1a;定制自己的种子&#xff0c;爬取想要的数据&#xff0c;做点力所能及的小分析。正好&#xff0c;这段时间宝宝出生&#xff0c;一边陪宝宝和宝妈&#xff0c;一边把自己做的这个豆瓣电影爬虫的数据采集部分跑起来。现在做一个概要的介绍和演示…

WebRTC视频 02 - 视频采集类 VideoCaptureModule

WebRTC视频 01 - 视频采集整体架构 WebRTC视频 02 - 视频采集类 VideoCaptureModule&#xff08;本文&#xff09; WebRTC视频 03 - 视频采集类 VideoCaptureDS 上篇 WebRTC视频 04 - 视频采集类 VideoCaptureDS 中篇 WebRTC视频 05 - 视频采集类 VideoCaptureDS 下篇 一、前言…

基于STM32的智能家居系统:MQTT、AT指令、TCP\HTTP、IIC技术

一、项目概述 随着智能家居技术的不断发展&#xff0c;越来越多的家庭开始使用智能设备来提升生活质量和居住安全性。智能家居系统不仅提供了便利的生活方式&#xff0c;还能有效地监测家庭环境&#xff0c;保障家庭安全。本项目以设计一种基于STM32单片机的智能家居系统为目标…

【汇编】c++游戏开发

由一起学编程创作的‘C/C项目实战&#xff1a;2D射击游戏开发&#xff08;简易版&#xff09;&#xff0c; 440 行源码分享来啦~’&#xff1a; C/C项目实战&#xff1a;2D射击游戏开发&#xff08;简易版&#xff09;&#xff0c; 440 行源码分享来啦~_射击c-CSDN博客文章浏览…

重构Action-cli前端脚手架

一、概述 最近一年&#xff0c;为了满足公司业务开发&#xff0c;解决重复搭建项目繁琐过程&#xff0c;自己开发了一个前端脚手架&#xff0c;并发布到npm。随着时间的推移&#xff0c;发现之前的版本存在很多问题&#xff0c;有些功能做不到位&#xff0c;而且代码也不是很规…

HuggingFace:基于YOLOv8的人脸检测模型

个人操作经验总结 1、YOLO的环境配置 github 不论base环境版本如何&#xff0c;建议在conda的虚拟环境中安装 1.1、创建虚拟环境 conda create -n yolov8-face python3.9conda create &#xff1a;创建conda虚拟环境&#xff0c; -n &#xff1a;给虚拟环境命名的…

基于BERT的情感分析

基于BERT的情感分析 1. 项目背景 情感分析&#xff08;Sentiment Analysis&#xff09;是自然语言处理的重要应用之一&#xff0c;用于判断文本的情感倾向&#xff0c;如正面、负面或中性。随着深度学习的发展&#xff0c;预训练语言模型如BERT在各种自然语言处理任务中取得了…

几何合理的分片段感知的3D分子生成 FragGen - 评测

FragGen 来源于 2024 年 3 月 25 日 预印本的文章&#xff0c;文章题目是 Deep Geometry Handling and Fragment-wise Molecular 3D Graph Generation&#xff0c; 作者是 Odin Zhang&#xff0c;侯廷军&#xff0c;浙江大学药学院。FragGen 是一个基于分子片段的 3D 分子生成模…