- 鼠标在图片上移动时显示“十字准星”
- 长按+拖拽+松开鼠标左键,展示一个框
- 点击选中框,被选中框有不同的颜色(下面示例中,自动选择了新生成的框)
最终效果示例:
说明:
- 自己造的轮子,不算完美,可能有疏忽的细节
- 已经处理了页面尺寸自适应,图片尺寸取决于父组件的容器大小,框的数据是相对于图片宽高的比例(0-1之间数字)
- 因为一开始需要读取图片的宽高,所以需要给图片设置id。也就是说,如果需要在同一页面使用2个以上这个组件,需要自己去把dom id打包,避免多个图片使用同一id。
组件代码:
<template>
<div
@mousedown="handleMouseDown"
@mouseup="handleMouseUp"
@mousemove="handleMouseMove"
@mouseover="handleMouseOver"
@mouseleave="handleMouseLeave"
class="container"
>
<img
v-if="imgPath.length"
id="img-marking"
:src="imgPath"
class="auto-size"
draggable="false"
@load="setSize"
/>
<div
v-show="showCross"
class="cross cross-vertical"
:style="{
height: `${crossHeight}px`,
top: `${containerTop}px`,
left: `${mouseX}px`,
}"
></div>
<div
v-show="showCross"
class="cross cross-horizontal"
:style="{
width: `${crossWidth}px`,
top: `${mouseY}px`,
left: `${containerLeft}px`,
}"
></div>
<template v-if="imgLoaded">
<div
v-for="(item, i) in rectList"
:key="i"
:class="i === selectedRectIndex ? 'rect rect-red' : 'rect rect-blue'"
:style="{
top: `${item.top * containerHeight + containerTop}px`,
left: `${item.left * containerWidth + containerLeft}px`,
width: `${(item.right - item.left) * containerWidth}px`,
height: `${(item.bottom - item.top) * containerHeight}px`,
}"
@click.stop="rectClick(i)"
></div>
<div
v-show="showDrawingRect"
class="rect rect-red"
:style="{
top: `${
Math.min(drawingPosition.startY, drawingPosition.endY) +
containerTop
}px`,
left: `${
Math.min(drawingPosition.startX, drawingPosition.endX) +
containerLeft
}px`,
width: `${Math.abs(drawingPosition.startX - drawingPosition.endX)}px`,
height: `${Math.abs(
drawingPosition.startY - drawingPosition.endY
)}px`,
}"
></div>
</template>
</div>
</template>
<script>
export default {
name: "ImageMarker",
props: {
imgPath: {
type: String,
required: true,
},
rectList: {
type: Array,
required: true,
},
selectedRectIndex: {
type: Number,
required: true,
},
minimumSize: {
type: Array,
default: () => [50, 50],
},
},
data() {
return {
imgLoaded: false,
showCross: false,
crossHeight: 0,
crossWidth: 0,
containerLeft: 0,
containerRight: 0,
containerTop: 0,
containerBottom: 0,
containerWidth: 0,
containerHeight: 0,
mouseX: 0,
mouseY: 0,
mouseOffset: 5,
lastMouseDown: [0, 0],
drawingRect: false,
drawingPosition: {},
};
},
computed: {
showDrawingRect() {
if (!this.drawingRect) {
return false;
}
if (
Math.abs(this.drawingPosition.startY - this.drawingPosition.endY) < 8 &&
Math.abs(this.drawingPosition.startY - this.drawingPosition.endY) < 8
) {
return false;
}
return true;
},
},
mounted() {
window.addEventListener("resize", this.setSize);
},
unmounted() {
window.removeEventListener("resize", this.setSize);
},
methods: {
// 根据图片尺寸设置准星长度+尺寸自适应
setSize() {
this.imgLoaded = true;
const container = document.getElementById("img-marking");
if (!container) return;
const { top, bottom, left, right } = container.getBoundingClientRect();
this.crossHeight = bottom - top;
this.crossWidth = right - left;
this.containerTop = top;
this.containerBottom = bottom;
this.containerLeft = left;
this.containerRight = right;
this.containerWidth = right - left;
this.containerHeight = bottom - top;
},
// 鼠标移动
handleMouseMove(e) {
this.showCross =
e.clientX < this.containerRight && e.clientY < this.containerBottom;
if (!this.showCross) {
return;
}
this.mouseX = Math.max(e.clientX - this.mouseOffset, this.containerLeft);
this.mouseY = Math.max(e.clientY - this.mouseOffset, this.containerTop);
if (this.drawingRect) {
this.drawingPosition = {
...this.drawingPosition,
endX: this.mouseX - this.containerLeft,
endY: this.mouseY - this.containerTop,
};
}
},
// 鼠标移进
handleMouseOver() {
this.showCross = true;
},
// 鼠标移出
handleMouseLeave() {
this.showCross = false;
},
// 鼠标按下
handleMouseDown(e) {
this.drawingRect = true;
this.lastMouseDown = [e.clientX, e.clientY];
this.drawingPosition = {
startX: this.mouseX - this.containerLeft - this.mouseOffset,
startY: this.mouseY - this.containerTop - this.mouseOffset,
endX: this.mouseX - this.containerLeft - this.mouseOffset,
endY: this.mouseY - this.containerTop - this.mouseOffset,
};
console.log(this.rectList);
},
// 鼠标按起
handleMouseUp(e) {
this.drawingRect = false;
if (
Math.abs(e.clientX - this.lastMouseDown[0]) < this.minimumSize[0] ||
Math.abs(e.clientY - this.lastMouseDown[1]) < this.minimumSize[1]
) {
return;
}
const { startX, startY, endX, endY } = this.drawingPosition;
const list = this.rectList;
list.push({
top: Math.min(startY, endY) / this.containerHeight,
bottom: Math.max(startY, endY) / this.containerHeight,
left: Math.min(startX, endX) / this.containerWidth,
right: Math.max(startX, endX) / this.containerWidth,
});
this.$emit("update:rectList", list);
this.$emit("update:selectedRectIndex", list.length - 1);
},
// 点击方块
rectClick(i) {
this.$emit("update:selectedRectIndex", i);
},
},
};
</script>
<style scoped>
.container {
width: 100%;
height: 100%;
user-select: none;
}
.auto-size {
max-width: 100%;
max-height: 100%;
}
.cross {
position: absolute;
background: #f85757;
z-index: 99;
}
.cross-vertical {
width: 2px;
}
.cross-horizontal {
height: 2px;
}
.rect {
position: absolute;
z-index: 50;
}
.rect-red {
border: 1px solid rgb(125, 5, 5);
background: rgba(125, 5, 5, 0.3);
}
.rect-blue {
border: 1px solid rgb(3, 38, 165);
background: rgba(3, 38, 165, 0.3);
}
</style>
引用示例:
- imgPath: 图片路径
- rectList: 所有框的位置数据
- selectedRectIndex当前选择的框的索引(rectList里的)
- minimumSize: 每个框的最小[宽,高]
<template>
<div style="width: 1200px; height: 900px">
<ImageMarker
:imgPath="imgPath"
:rectList.sync="rectList"
:selectedRectIndex.sync="selectedRectIndex"
:minimumSize="[50, 50]"
/>
</div>
</template>
<script>
import ImageMarker from "@/components/ImageMarker.vue";
export default {
name: "HomeView",
components: {
ImageMarker,
},
data() {
return {
imgPath: "",
rectList: [],
selectedRectIndex: -1,
};
},
mounted() {
this.imgPath = "test.jpg";
this.rectList = [
{
bottom: 0.81446331360189,
left: 0.2829747427502339,
right: 0.5028063610851263,
top: 0.3749187064749361,
},
{
bottom: 0.7,
left: 0.55,
right: 0.9,
top: 0.6,
},
];
this.selectedRectIndex = 1;
},
};
</script>