功能点
- 底图: 可缩放,移动,重置,楼层切换
- 点位:可增加,删除,二次拖动,自定义图标
- 演示视频和源码下载 请拉到最后,功能截图如下:

知识点
- HTML的
dragstart
和drop
事件(细节可以参考 MDN),以及鼠标的onmousemove
,onmouseup
,oncontextmenu
等事件 - 鼠标距离可视区边缘的的位置
clientX/clientY
,元素相对于父元素的偏移量offsetLeft/offsetTop
,元素距离可视区边缘的的大小和位置信息getBoundingClientRect
- CSS的
scale
属性,以及一些位置偏移量的矫正运算
逻辑流程
- 先给右边的树节点绑定
dragstart
事件,将需要拖拽的数据当参数传入函数
html
<el-tree
ode-key="id"
default-expand-all
:highlight-current="true"
:expand-on-click-node="false"
:data="deviceTree"
><template #default="{ node, data }">
<!-- https://developer.mozilla.org/zh-CN/docs/Web/API/DataTransfer/setDragImage -->
<span :draggable="true" @dragstart="dragstart($event, data)">
<span>{{ node.label }}{{ node.url }}</span>
<span>
<img class="drag-icon" :src="data.url" />
</span>
</span>
</template>
</el-tree>
- 一般点击dom节点进行拖动的话,图标是默认选中所有子节点,这里我们需要通过
dataTransfer.setDragImage
自定义一个icon图标(不然会发现拖动了一整个树节点,导致图标过大无法打点),并且设置 一个标识属性dragFromFather
,告诉子组件这次拖拽过来的节点来源是父组件,而不是其他地方。最后,通过dataTransfer.setData
设置需要带过去的数据
js
const dragstart = (e: any, data: Marker) => {
dragIcon.value = e.target.children[1]
e.dataTransfer.setDragImage(dragIcon.value, 10, 10)
data.dragFromFather = true // 告诉子组件拖拽事件的来源
let temp = toRaw(data)
e.dataTransfer.setData('text/plain', JSON.stringify(temp))
}
- 重点在子组件( 必须给拖放区元素添加
dragover.prevent
,才能使drop
事件正确执行)
html
<div class="container" @dragover.prevent @drop="ondrop" @contextmenu="oncontextmenu">
<div
ref="floorMap"
class="floorMap"
:style="{
transform: `scale(${num})`,
cursor: continuous ? 'none' : 'default'
}"
@mousewheel="zoomMap"
@mousedown="clickMap"
>
<!-- click在鼠标释放时触发,所以要用mousedown,在按下的时候就赋值 -->
<img
v-for="(marker, index) in markers"
@mousedown="markerClick(marker)"
@mouseenter="showPopover($event, marker)"
@mouseleave="hidePopover()"
:style="markerStyle(marker)"
:key="index"
class="marker-class"
:src="marker.url"
/>
<img class="base-map" :src="currentFloorUrl" />
</div>
<div
v-if="popoverVisible"
class="popover"
:style="popoverStyle(popoverInfo)"
@mouseenter="slideIntoMark"
@mouseleave="slideOutMark"
>
<div class="content">
<span>点位{{ popoverInfo?.id }}</span>
<span @click="deleteMarker" class="delete">删除</span>
</div>
<div class="popover-arrow"></div>
</div>
<img
v-if="continuous"
class="customize-mouse-shape"
ref="customizeMouseShape"
src="../../assets/dot/dot.png"
/>
</div>
以上就是子组件的所有dom结构,围绕这些结构介绍下主要的功能实现:
(1)drop
事件用来接收拖拽过来的数据,同时设置连续打点的开关continuous
为true
,以及绑定onmousemove
来和onclick
事件来实现连续打点,setCustomizeMouse
是用来设置连续打点时鼠标的位置,setMarkerPosition
是用来设置点位的坐标
js
const ondrop = (e: any) => {
if (!e.dataTransfer.getData('text/plain')) return // 是否携带数据
let dragData = JSON.parse(e.dataTransfer.getData('text/plain'))
if (!dragData.dragFromFather) return // 是否来自父组件的拖拽
let odiv = e.target
setMarkerPosition(e, odiv, dragData)
if (props.continuousPoints.length === 1) return // 如果只有一个就不用开启连续打点
continuous.value = true
proxy.$refs.floorMap.onmousemove = (e: any) => {
setCustomizeMouse(e)
}
// 鼠标左键点击
document.onclick = (e: any) => {
if (props.continuousPoints.length === 1) {
endContinuous()
}
let dragData = props.continuousPoints[0]
setMarkerPosition(e, odiv, dragData)
}
}
(2) 鼠标右键事件oncontextmenu
,用来禁止默认事件,以及中断连续打点
js
const oncontextmenu = (e: any) => {
e.preventDefault()
endContinuous()
}
(3) mousewheel
和 mousedown
分别绑定鼠标的滚轮和点击事件,num
控制底图的缩放值,如果二次拖动了点位,通过changeMarkers
来实时更新并记录位置
js
//缩放底图
const zoomMap = (e: any) => {
draggedMap.value = true
popoverVisible.value = false
if (e.deltaY > 0) {
if (num.value < 0.5) return
num.value -= 0.1
} else {
num.value += 0.1
}
}
const clickMap = (e: any) => {
let odiv = e.target
let scale = draggingMarker.value ? num.value : 1 // draggingMarker 判断拖拽的是底图还是标记点,后者的话加上num当系数
zoomedMap.value = !draggingMarker.value
popoverVisible.value = false
// 计算鼠标距离底图左边界的初始距离
let disX = e.clientX - odiv.offsetLeft * scale
let disY = e.clientY - odiv.offsetTop * scale
document.onmousemove = (e) => {
let left = e.clientX - disX
let top = e.clientY - disY
odiv.style.left = left / scale + 'px'
odiv.style.top = top / scale + 'px'
}
document.onmouseup = (e: any) => {
if (draggingMarker.value) {
let left = e.clientX - disX
let top = e.clientY - disY
let markers = JSON.parse(JSON.stringify(toRaw(props.markers)))
markers.map((v: Marker) => {
if (v.id == draggingMarker.value?.id) {
v.top = top / scale
v.left = left / scale
}
})
emitEvents('changeMarkers', markers)
draggingMarker.value = null
}
document.onmousemove = null
document.onmouseup = null
isDragging.value = false
}
}
(4)markerStyle
用来更新点位的状态,并加上num
系数控制图标大小
js
const markerStyle = (marker: Marker): any => {
return {
position: 'absolute',
left: marker.left + 'px',
top: marker.top + 'px',
scale: 1 / num.value,
width: markerWidth.value + 'px'
}
}
(5) 上述提到的 setCustomizeMouse
和 setMarkerPosition
js
// 设置连续打点时的鼠标位置
const setCustomizeMouse = (e: any) => {
proxy.$refs.customizeMouseShape.style.left = e.clientX - 10 + 'px'
proxy.$refs.customizeMouseShape.style.top = e.clientY - 10 + 'px'
}
// 拖拽过来之后设置点位
const setMarkerPosition = (e: any, odiv: any, dragData: Marker) => {
let markers = JSON.parse(JSON.stringify(toRaw(props.markers)))
markers.push(
Object.assign(dragData, {
top: (e.clientY - odiv.getBoundingClientRect().y) / num.value - markerWidth.value / 2,
left: (e.clientX - odiv.getBoundingClientRect().x) / num.value - markerWidth.value / 2
})
)
emitEvents('getContinuousPoints', dragData.id)
emitEvents('changeMarkers', markers)
}
(6)弹窗的逻辑比较直白,就不做解释了,另外点位信息存在了localstorage里,如果不请空,刷新页面后打的点位依旧会存在
在线演示和源码
需要演示地址和源码的小伙伴请 点击 , 演示视频如下:

结语
文字可介绍的内容不多,主要是在代码层面,关键代码都做了注释,源码下载即可运行,无需额外配置
另外,这个功能是从项目里抽离出来的,其实还有 点位定位, 告警点位闪烁,摄像头视频接入等功能,但是万变不离其宗,这些后续内容都是在该基础上堆起来的
欢迎小伙伴留言讨论,互相学习!
❤❤❤ 如果对你有帮助,记得点赞收藏哦!❤❤❤