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