vue實(shí)現(xiàn)一個(gè)矩形標(biāo)記區(qū)域(rectangle marker)的方法
代碼地址:vue-rectangle-marker
一、前言
一些cms系統(tǒng)經(jīng)常會(huì)用到區(qū)域標(biāo)記功能,所以寫了個(gè)用vue實(shí)現(xiàn)的矩形標(biāo)記區(qū)域,包含拖拽、放大縮小、重置功能。
二、實(shí)現(xiàn)結(jié)果
1.初始

2.標(biāo)記

三、代碼實(shí)現(xiàn)
<template>
<div class="rectangle-marker">
<div class="mark-wrap">
<img ref="backImg" :src="imgUrl" class="img-responsive" alt="響應(yīng)式圖像" @load="onload">
<div class="draw-rect" :class="{ 'no-event': disabled }" @mousemove="mouseMove"
@mousedown="mouseDown" @mouseup="mouseUp">
<div ref="box" v-if="boxVisible" :id="boxId" class="box"
:style="{ width: boxW + 'px', height: boxH + 'px', left: boxL + 'px', top: boxT + 'px' }">
<div id="upleftbtn" class="upleftbtn" @mousedown="onUpleftbtn"></div>
<div id="uprightbtn" class="uprightbtn" @mousedown="onUpRightbtn"></div>
<div id="downleftbtn" class="downleftbtn" @mousedown="onDownleftbtn"></div>
<div id="downrightbtn" class="downrightbtn" @mousedown="onDownRightbtn"></div>
</div>
</div>
<transition name="fade">
<div v-if="showBtns && !markFlag" class="act-btns" @mouseleave="mouseLeave">
<button @click="mark">mark</button>
<button @click="reset">reset</button>
</div>
</transition>
</div>
</div>
</template>
<script>
export default {
name: 'rectangleMarker',
data() {
return {
imgW: 0,
imgH: 0,
showBtns: true,
markFlag: false,
// 鼠標(biāo)事件屬性
dragging: false,
startX: undefined,
startY: undefined,
diffX: undefined,
diffY: undefined,
obj: null, //當(dāng)前操作對(duì)象
box: null, //要處理的對(duì)象
backImgRect: null,
boxId: '',
boxW: 0,
boxH: 0,
boxL: 0,
boxT: 0,
boxVisible: false
}
},
props: {
imgUrl: {
type: String,
required: true,
default: ''
},
disabled: {
type: Boolean,
default: false
},
value: {
type: Array,
default: function () {
return []
}
}
},
methods: {
onload() {
let rect = this.$refs.backImg.getBoundingClientRect()
this.backImgRect = {
height: rect.height,
width: rect.width
}
// console.log("initConfig -> this.backImgRect", this.backImgRect)
if (this.value === '' || this.value === undefined || this.value === null || (Array.isArray(this.value) && this.value.length === 0)) {
return
}
this.initData(this.value)
},
mouseLeave() {
this.showBtns = false
},
mark() {
this.markFlag = true
},
reset() {
this.boxVisible = false
this.boxId = ''
this.boxH = 0
this.boxW = 0
this.boxL = 0
this.boxT = 0
},
initData(data) {
if (data === '' || data === undefined || data === null || (Array.isArray(data) && data.length === 0)) {
return
}
this.boxId = 'changeBox'
this.boxL = data[0][0] * this.backImgRect.width
this.boxT = data[0][1] * this.backImgRect.height
this.boxH = (data[3][1] - data[0][1]) * this.backImgRect.height
this.boxW = (data[1][0] - data[0][0]) * this.backImgRect.width
this.boxVisible = true
},
mouseDown(e) {
if (!this.markFlag && !this.boxVisible) {
return
}
this.startX = e.offsetX;
this.startY = e.offsetY;
// 如果鼠標(biāo)在 box 上被按下
if (e.target.className.match(/box/)) {
// 允許拖動(dòng)
this.dragging = true;
// 設(shè)置當(dāng)前 box 的 id 為 movingBox
if (this.boxId !== 'movingBox') {
this.boxId = 'movingBox'
}
// 計(jì)算坐標(biāo)差值
this.diffX = this.startX
this.diffY = this.startY
} else {
if (this.boxId === 'changeBox') {
return
}
this.boxId = 'activeBox'
this.boxT = this.startY
this.boxL = this.startX
this.boxVisible = true
}
},
mouseMove(e) {
if (!this.markFlag && !this.boxVisible) {
if (!this.backImgRect) {
return
}
let toRight = this.backImgRect.width - e.offsetX
let toTop = e.offsetY
if (toRight <= 100 && toTop <= 40) {
this.showBtns = true
}
return
}
let toRight = this.backImgRect.width - e.offsetX
let toTop = e.offsetY
if (toRight <= 100 && toTop <= 40) {
this.showBtns = true
return
}
// 更新 box 尺寸
if (this.boxId === 'activeBox') {
this.boxW = e.offsetX - this.startX
this.boxH = e.offsetY - this.startY
}
// 移動(dòng),更新 box 坐標(biāo)
if (this.boxId === 'movingBox' && this.dragging) {
let realTop = (e.offsetY + e.target.offsetTop - this.diffY) > 0 ? (e.offsetY + e.target.offsetTop -
this.diffY) : 0
let realLeft = (e.offsetX + e.target.offsetLeft - this.diffX) > 0 ? (e.offsetX + e.target.offsetLeft -
this.diffX) : 0
let maxTop = this.backImgRect.height - this.$refs.box.offsetHeight
let maxLeft = this.backImgRect.width - this.$refs.box.offsetWidth
realTop = realTop >= maxTop ? maxTop : realTop
realLeft = realLeft >= maxLeft ? maxLeft : realLeft
this.boxT = realTop;
this.boxL = realLeft;
}
if (this.obj) {
e = e || window.event;
var location = {
x: e.x || e.offsetX,
y: e.y || e.offsetY
}
switch (this.obj.operateType) {
case "nw":
this.move('n', location, this.$refs.box);
this.move('w', location, this.$refs.box);
break;
case "ne":
this.move('n', location, this.$refs.box);
this.move('e', location, this.$refs.box);
break;
case "sw":
this.move('s', location, this.$refs.box);
this.move('w', location, this.$refs.box);
break;
case "se":
this.move('s', location, this.$refs.box);
this.move('e', location, this.$refs.box);
break;
case "move":
this.move('move', location, this.box);
break;
}
}
},
mouseUp() {
if (!this.markFlag && !this.boxVisible) {
return
}
// 禁止拖動(dòng)
this.dragging = false;
if (this.boxId === 'activeBox') {
if (this.$refs.box) {
this.boxId = 'changeBox'
if (this.$refs.box.offsetWidth < 3 || this.$refs.box.offsetHeight < 3) {
this.boxVisible = false
this.boxId = ''
}
}
} else {
if (this.$refs.box && this.boxId === 'movingBox') {
this.boxId = 'changeBox'
if (this.$refs.box.offsetWidth < 3 || this.$refs.box.offsetHeight < 3) {
this.boxVisible = false
this.boxId = ''
}
}
}
if (this.boxVisible) {
this.getHotData()
document.body.style.cursor = "auto";
this.obj = null;
this.markFlag = false
} else {
this.markFlag = true
}
},
getHotData() {
let target = this.$refs.box
if (target) {
let {
offsetTop,
offsetLeft
} = target
let {
width: WIDTH,
height: HEIGHT
} = this.backImgRect
let {
width,
height
} = target.getBoundingClientRect()
// 矩形區(qū)域 角點(diǎn)位置(百分比)
let data = [
[this.toFixed6(offsetLeft, WIDTH), this.toFixed6(offsetTop, HEIGHT)],
[this.toFixed6(offsetLeft + width, WIDTH), this.toFixed6(offsetTop, HEIGHT)],
[this.toFixed6(offsetLeft + width, WIDTH), this.toFixed6(offsetTop + height, HEIGHT)],
[this.toFixed6(offsetLeft, WIDTH), this.toFixed6(offsetTop + height, HEIGHT)]
]
// 矩形中點(diǎn)
let centerPoint = [
this.toFixed6(offsetLeft + 0.5 * width, WIDTH),
this.toFixed6(offsetTop + 0.5 * height, HEIGHT)
]
let hotData = {
data,
centerPoint
}
console.log("getHotData -> hotData", hotData)
console.log(JSON.stringify(hotData));
}
},
toFixed6(v1, v2) {
return (v1 / v2).toFixed(6)
},
move(type, location, tarobj) {
switch (type) {
case 'n': {
let add_length = this.clickY - location.y;
this.clickY = location.y;
let length = parseInt(tarobj.style.height) + add_length;
tarobj.style.height = length + "px";
let realTop = this.clickY > 0 ? this.clickY : 0
let maxTop = this.backImgRect.height - parseInt(tarobj.style.height)
realTop = realTop >= maxTop ? maxTop : realTop
tarobj.style.top = realTop + "px";
break;
}
case 's': {
let add_length = this.clickY - location.y;
this.clickY = location.y;
let length = parseInt(tarobj.style.height) - add_length;
let maxHeight = this.backImgRect.height - parseInt(tarobj.style.top)
let realHeight = length > maxHeight ? maxHeight : length
tarobj.style.height = realHeight + "px";
break;
}
case 'w': {
var add_length = this.clickX - location.x;
this.clickX = location.x;
let length = parseInt(tarobj.style.width) + add_length;
tarobj.style.width = length + "px";
let realLeft = this.clickX > 0 ? this.clickX : 0
let maxLeft = this.backImgRect.width - parseInt(tarobj.style.width)
realLeft = realLeft >= maxLeft ? maxLeft : realLeft
tarobj.style.left = realLeft + "px";
break;
}
case 'e': {
let add_length = this.clickX - location.x;
this.clickX = location.x;
let length = parseInt(tarobj.style.width) - add_length;
let maxWidth = this.backImgRect.width - parseInt(tarobj.style.left)
let realWidth = length > maxWidth ? maxWidth : length
tarobj.style.width = realWidth + "px";
break;
}
}
},
onUpleftbtn(e) {
e.stopPropagation();
this.onDragDown(e, "nw");
},
onUpRightbtn(e) {
e.stopPropagation();
this.onDragDown(e, "ne");
},
onDownleftbtn(e) {
e.stopPropagation();
this.onDragDown(e, "sw");
},
onDownRightbtn(e) {
e.stopPropagation();
this.onDragDown(e, "se");
},
onDragDown(e, type) {
e = e || window.event;
this.clickX = e.x || e.offsetX;
this.clickY = e.y || e.offsetY;
this.obj = window;
this.obj.operateType = type;
this.box = this.$refs.box;
return false;
}
},
}
</script>
<style lang="less" scoped>
.rectangle-marker {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
.mark-wrap {
position: relative;
.img-responsive {
display: inline-block;
max-width: 100%;
max-height: 100%;
}
.draw-rect {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
width: 100%;
height: 100%;
z-index: 99;
user-select: none;
&.no-event {
pointer-events: none;
}
}
}
.act-box {
margin-top: 10px;
display: flex;
}
.act-btns {
position: absolute;
right: 0;
top: 0;
z-index: 199;
padding: 0 10px;
height: 40px;
width: 100px;
display: flex;
align-items: center;
justify-content: center;
}
.fade-enter-active {
animation: hide-and-show .5s;
}
.fade-leave-active {
animation: hide-and-show .5s reverse;
}
@keyframes hide-and-show {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
}
</style>
<style lang="less">
.rectangle-marker {
.box {
position: absolute;
width: 0px;
height: 0px;
opacity: 0.5;
z-index: 149;
cursor: move;
border: 1px solid #f00;
.upleftbtn,
.uprightbtn,
.downleftbtn,
.downrightbtn {
width: 10px;
height: 10px;
border: 1px solid steelblue;
position: absolute;
z-index: 5;
background: whitesmoke;
border-radius: 10px;
}
.upleftbtn {
top: -5px;
left: -5px;
cursor: nw-resize;
}
.uprightbtn {
top: -5px;
right: -5px;
cursor: ne-resize;
}
.downleftbtn {
left: -5px;
bottom: -5px;
cursor: sw-resize;
}
.downrightbtn {
right: -5px;
bottom: -5px;
cursor: se-resize;
}
}
}
</style>
- 背景圖傳入,圖片自適應(yīng)處理。
- 定義drag標(biāo)記為,添加開始標(biāo)記、重置按鈕。
- 創(chuàng)建box區(qū)域,不同狀態(tài)(change、moving、active),對(duì)應(yīng)不同id。
- box可移動(dòng)距離,計(jì)算邊界。
- 四角放大縮小的功能。
- 生成結(jié)果,精確到6位小數(shù),這樣可以使得復(fù)原標(biāo)記區(qū)域的時(shí)候誤差最小。
四、覺得有幫助的,麻煩給個(gè)贊哦,謝謝!
以上就是vue實(shí)現(xiàn)一個(gè)矩形標(biāo)記區(qū)域(rectangle marker)的方法的詳細(xì)內(nèi)容,更多關(guān)于vue實(shí)現(xiàn)矩形標(biāo)記區(qū)域的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Vue el-upload單圖片上傳功能實(shí)現(xiàn)
這篇文章主要介紹了Vue el-upload單圖片上傳功能實(shí)現(xiàn),本文通過示例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-11-11
實(shí)現(xiàn)一個(gè)VUE響應(yīng)式屬性裝飾器詳析
這篇文章主要介紹了實(shí)現(xiàn)一個(gè)VUE響應(yīng)式屬性裝飾器詳析,文章通過圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的小伙伴可以參考一下2022-09-09
vue項(xiàng)目中如何配置env環(huán)境的實(shí)現(xiàn)
本文主要介紹了vue項(xiàng)目中如何配置env環(huán)境的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-07-07
bootstrap vue.js實(shí)現(xiàn)tab效果
這篇文章主要為大家詳細(xì)介紹了bootstrap vue.js實(shí)現(xiàn)tab效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-02-02
vue?實(shí)現(xiàn)動(dòng)態(tài)設(shè)置元素的高度
這篇文章主要介紹了在vue中實(shí)現(xiàn)動(dòng)態(tài)設(shè)置元素的高度,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-08-08
vue實(shí)現(xiàn)token登錄驗(yàn)證的完整實(shí)例
最近公司新啟動(dòng)了個(gè)項(xiàng)目,用的是vue框架在做,下面這篇文章主要給大家介紹了關(guān)于vue實(shí)現(xiàn)token登錄驗(yàn)證的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-04-04
Vue實(shí)現(xiàn)點(diǎn)擊圖片放大顯示功能
這篇文章主要為大家詳細(xì)介紹了如何利用Vue實(shí)現(xiàn)點(diǎn)擊圖片放大顯示功能,文中的示例代碼講解詳細(xì),具有一定的參考價(jià)值,感興趣的可以了解一下2023-03-03
vue中更改數(shù)組中屬性,在頁(yè)面中不生效的解決方法
今天小編就為大家分享一篇vue中更改數(shù)組中屬性,在頁(yè)面中不生效的解決方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2019-10-10

