前端手动框选图片热区区域,实现热区的随意放大、缩小、拖动排版
本文介绍了一种图片热区框选功能的实现方案,旨在解决运营活动中重复开发海报页的问题。通过在前端实现鼠标交互(mousedown、mousemove、mouseup)来框选热区,并支持双击绑定跳转事件。核心代码采用Vue实现,包含热区绘制、位置调整、删除等功能组件。该方案适用于简单的海报引流场景,使运营人员可自主配置热区跳转,减少开发工作量。文中详细展示了热区计算的逻辑和组件实现代码,包括坐标处理、边
前言:开发苦运营活动久矣,运营亦吐槽开发繁多;凡遇活动皆需从头开发,时间紧、任务急,耗资甚多,全员疲惫,苦不堪言!值此危急存亡之秋,吾挺身而出(我来!!!)。在后台搞一个图片热区框选的功能,你们运营自己去后台框框选选,配置好想跳转的功能,自己想怎么搞怎么搞去吧!
备注:此功能适用于活动不是很复杂,且没有复杂交互的活动页,即海报页。通常用于运营海报页活动引流至某些功能或者配置一些不固定文案的场景(提供个思路,如年终总结之类)
进入正文:
提高看客效率,咱先贴图,不是你的菜,看完图就撤,就当您没来过!哈哈(网上随便找的海报示例图)
这是上传的原图:
这是进行热区框选之后的样子:
从上图可以发现,经过了鼠标框选,我们需要的热区就已经确定好了,剩下的就是双击热区进行对应事件的绑定配置了,这里就结合自身的业务具体来了。这里我们只讲核心——热区框选的交互
基本思路就是:1.鼠标点击左键事件mousedown(开始);2.鼠标滑动事件onmousemove;3.根据鼠标滑动坐标计算热区的区域;4.鼠标抬起mouseup,绘制热区结束,得到最终的热区对象
上代码说事吧
<template>
<div>
<div class="img_box" @mouseup.left.stop="changeStop()">
<div class="container" id="img-box-container">
<img
ref="backgroundImg"
:src="imgUrl"
ondragstart="return false;"
oncontextmenu="return false;"
onselect="document.selection.empty();"
alt=""
@mousedown.left.stop="mouseDown($event)"
/>
<!--绘制的热区-->
<div
v-show="caseShow"
class="area"
:style="{
width: areaWidth + 'px',
height: areaHeight + 'px',
left: starX + 'px',
top: starY + 'px',
}"
/>
<HotAreaBox
v-for="(item, index) in areaData"
:index="index"
:key="index"
:areaInit="item"
:parent-width="parentWidth"
:parent-height="parentHeight"
@updateArea="updateArea"
@delAreaBox="delAreaBox"
></HotAreaBox>
</div>
</div>
</div>
</template>
<script lang="ts">
import {Component, Emit, Prop, Vue, Watch} from 'vue-property-decorator';
import HotAreaBox from '../hotAreaBox/hotAreaBox.vue';
@Component({
components: {
HotAreaBox,
},
})
export default class ImageHot extends Vue {
@Prop({
default: '',
})
imgUrl!: string;
@Prop({
default:[]
})
areaDataList!: any;
starX = 0;
// endX: 0;
starY = 0;
// endY = 0;
areaWidth = 0;
areaHeight = 0;
parentWidth = 0;
parentHeight = 0;
caseShow = false;
nowImgWidth = null;
areaData: any = [];
mounted () {
const parentDiv: any = document.querySelector("#img-box-container");
//获取元素的宽高
this.parentWidth = parentDiv.clientWidth;
this.parentHeight = parentDiv.clientHeight;
this.areaData = this.areaDataList
console.log(this.areaData,"000000000000")
}
// 绘画热区开始
mouseDown(e: any) {
const backgroundImg: any = this.$refs.backgroundImg;
// 显示热区
this.caseShow = true;
// 记录滑动的初始值
// this.starX = e.offsetX;
// this.starY = e.offsetY;
this.starX = e.layerX;
this.starY = e.layerY;
// 鼠标滑动的过程
if (!document.onmousemove) {
document.onmousemove = (ev: any) => {
if(ev.layerX<=backgroundImg.width&&ev.layerY<=backgroundImg.height){
this.areaWidth = ev.layerX - this.starX;
this.areaHeight = ev.layerY - this.starY;
}else{
this.changeStop()
}
};
}
}
// 绘画热区结束
changeStop() {
document.onmousemove = null;
if (this.caseShow && this.areaWidth > 10 && this.areaHeight > 10) {
const backgroundImg: any = this.$refs.backgroundImg;
const obj = {
starX: this.starX,
starY: this.starY,
areaWidth: this.areaWidth,
areaHeight: this.areaHeight,
nowImgWidth: backgroundImg.width,
nowImgHeight:backgroundImg.height,
};
this.areaData.push(obj);
}
// 初始化绘图
this.caseShow = false;
this.starX = 0;
this.starY = 0;
this.areaWidth = 0;
this.areaHeight = 0;
}
// 删除指定热区
delAreaBox(index: number) {
console.log(index,"索引值")
setTimeout(() => {
this.areaData.splice(index, 1);
}, 300);
}
// 更新热区
updateArea(obj: any){
this.areaData[obj.index] = obj
console.log(this.areaData)
this.updateAreaData()
}
// 通知父组件更新热区数据
updateAreaData(){
this.$emit('updateAreaDataList', this.areaData);
}
}
</script>
<style lang="less" scoped>
.img_box{
max-height: 1000px;
overflow-y: auto;
margin: 0 auto;
display: flex;
flex-direction: column;
align-items: center;
.container{
position: relative;
img{
width: 750px;
cursor: crosshair;
}
.area{
position: absolute;
width: 200px;
height: 200px;
left: 200px;
top: 300px;
background: rgba(#2980b9, 0.3);
border: 1px dashed #34495e;
}
}
}
</style>
热区区域组件的代码:
<template>
<div>
<div
class="areaBox"
:style="{width: areaInit.areaWidth+'px',height: areaInit.areaHeight+'px',left: areaInit.starX+'px',top: areaInit.starY+'px'}"
@dblclick="doubleClickSet"
@mousedown.left.stop="mouseDown($event)"
@mouseup.left.stop="mouseUp($event)"
>
<div class="area_name" :title="'热区名称:'+areaInit.hotAreaName">热区名称:{{areaInit.hotAreaName}}</div>
<span class="promptText">双击{{areaInit.pathMsg?'编辑':'设置'}}热区</span>
<div class="area_path" v-if="areaInit.pathMsg" :title="'跳转:'+areaInit.pathMsg.pathName">跳转:{{areaInit.pathMsg.pathName}}</div>
<!--删除-->
<div class="del" @click.stop="del()">X</div>
<!--形变点-->
<div class="shape" @mousedown.left.stop="shapeDown($event)" @mouseup.left.stop="mouseUp($event)"/>
</div>
</div>
</template>
<script lang="ts">
import {Component, Emit, Prop, Vue, Watch} from 'vue-property-decorator';
import { namespace } from 'vuex-class';
const { State, Action, Mutation } = namespace('activityConfig/activityManage');
@Component({
components: {
},
})
export default class AreaBox extends Vue {
@Prop({
default: {},
})
areaInit: any;
@Prop({
default:0
})
parentWidth!: number
@Prop({
default:0
})
parentHeight!: number
@Prop({
default:null,
})
index!: number
starX = 0;
starY = 0;
star1X = 0;
star1Y = 0;
// box操作初始点
move = {
// 拖动
startX: 0,
starY: 0,
// 形变
start1X: 0,
start1Y: 0
};
dirty = false;
hotAreaModal = false;
get form1() {
return this.$form.createForm(this, {
onFieldsChange: () => {
this.dirty = true;
},
});
}
mounted () {
}
// 开始拖动
mouseDown(e: any) {
console.log(e)
this.starX = e.clientX;
this.starY = e.clientY;
if (!document.onmousemove) {
const initX = this.areaInit.starX
const initY = this.areaInit.starY
document.onmousemove = (ev: any) => {
// console.log(ev)
const position = {
nowX: initX + ev.clientX - this.starX,
nowY:initY + ev.clientY - this.starY
}
if(position.nowX<0){//说明热区超出图片左侧区域了
return false
}
if(position.nowY<0){//说明热区超出图片顶部区域了
return false
}
const nowRight = position.nowX + this.areaInit.areaWidth;
const nowBottom = position.nowY + this.areaInit.areaHeight;
console.log(nowBottom,this.areaInit.nowImgHeight,"0000")
if(nowRight>=this.areaInit.nowImgWidth){
position.nowX = this.areaInit.nowImgWidth - this.areaInit.areaWidth-10
}
if(nowBottom>=this.areaInit.nowImgHeight){
position.nowY = this.areaInit.nowImgHeight - this.areaInit.areaHeight
}
this.areaInit.starX = position.nowX
this.areaInit.starY = position.nowY
}
}
}
// 结束拖动/变形
mouseUp() {
if(document.onmousemove){
document.onmousemove = null
this.areaInit.index = this.index
this.$emit('updateArea', this.areaInit);
}
}
// 形变开始
shapeDown(e: any) {
this.star1X = e.clientX
this.star1Y = e.clientY
if (!document.onmousemove) {
const initX = this.areaInit.areaWidth
const initY = this.areaInit.areaHeight
document.onmousemove = (ev: any) => {
if(ev.layerX<=this.areaInit.nowImgWidth&&ev.layerY<=this.areaInit.nowImgHeight){
this.areaInit.areaWidth = initX + ev.clientX - this.star1X-10
this.areaInit.areaHeight = initY + ev.clientY - this.star1Y
}else{
this.mouseUp()
}
// this.areaInit.areaWidth = initX + ev.clientX - this.star1X
// this.areaInit.areaHeight = initY + ev.clientY - this.star1Y
}
}
}
// 删除
del() {
this.$emit('delAreaBox', this.index)
}
// 双击进行热区路径设置
doubleClickSet(){
document.onmousemove = null
}
// 添加/编辑热区跳转路径
confirmHotAreaPath(){
if(!this.hotAreaName){
return this.$message.warning('请输入热区名称')
}
this.addHotAreaPath()
}
addHotAreaPath(){
this.hotAreaModal = false
switch (this.funType) {
case 0://医生列表
this.areaInit.hotAreaName = this.hotAreaName,
this.areaInit.pathMsg = {
pathType:this.funType,
pathName:this.pathTypeText(this.funType)
}
this.$emit('updateArea', this.areaInit);
break;
case 1://医生主页
this.areaInit.hotAreaName = this.hotAreaName,
this.areaInit.pathMsg = {
pathType:this.funType,
pathName:this.pathTypeText(this.funType)+'-'+this.doctorData.name,
defaultValue:this.doctorDefault,
valueData:this.doctorData
}
this.$emit('updateArea', this.areaInit);
break;
default:
break;
}
}
}
</script>
<style lang="less" scoped>
.areaBox {
position: absolute;
background: rgba(#2980b9, 0.3);
border: 0.7px dashed #34495e;
display: flex;
justify-content: center;
align-items: center;
color: #34495e;
font-size: 14px;
cursor: move;
.promptText {
overflow: hidden;
display: inline;
max-width: 100%;
max-height: 100%;
text-align: center;
}
.area_name{
width:100%;
position: absolute;
top: 0;
left: 0px;
font-size: 12px;
color: #666666;
padding-left: 10px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.area_path{
width:100%;
position: absolute;
bottom: 0;
left: 0px;
font-size: 12px;
color: #666666;
padding-left: 10px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.del {
width: 20px;
height: 20px;
background: #bdc3c7;
border-radius: 50%;
position: absolute;
right: 0;
top: 0;
transform: translate3d(50%, -50%, 0);
cursor: pointer;
text-align: center;
line-height: 20px;
color: red;
}
.del:hover {
background: #ecf0f1;
}
.shape {
position: absolute;
width: 7px;
height: 7px;
background: transparent;
right: 0;
bottom: 0;
transform: translate3d(50%, 50%, 0);
cursor: nwse-resize;
}
}
</style>
附言:方法流程代码已完整提供,仅供参考,也不可能复制了直接就能跑通的,总归是需要结合自身情况替换对应的某些参数来的。
更多推荐
所有评论(0)