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},// 图片 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是宽高不固定,可以改成响应式的
父组件中:
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;},},