vue3图片打点,可在线演示,下载源码

功能点

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

知识点

  1. HTML的dragstartdrop事件(细节可以参考 MDN),以及鼠标的 onmousemoveonmouseuponcontextmenu等事件
  2. 鼠标距离可视区边缘的的位置clientX/clientY,元素相对于父元素的偏移量offsetLeft/offsetTop,元素距离可视区边缘的的大小和位置信息getBoundingClientRect
  3. CSS的 scale属性,以及一些位置偏移量的矫正运算

逻辑流程

  1. 先给右边的树节点绑定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>
  1. 一般点击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))
}
  1. 重点在子组件( 必须给拖放区元素添加 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 事件用来接收拖拽过来的数据,同时设置连续打点的开关continuoustrue,以及绑定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) mousewheelmousedown 分别绑定鼠标的滚轮和点击事件,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) 上述提到的 setCustomizeMousesetMarkerPosition

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里,如果不请空,刷新页面后打的点位依旧会存在

在线演示和源码

需要演示地址和源码的小伙伴请 点击 , 演示视频如下:

结语

文字可介绍的内容不多,主要是在代码层面,关键代码都做了注释,源码下载即可运行,无需额外配置

另外,这个功能是从项目里抽离出来的,其实还有 点位定位, 告警点位闪烁,摄像头视频接入等功能,但是万变不离其宗,这些后续内容都是在该基础上堆起来的

欢迎小伙伴留言讨论,互相学习!

❤❤❤ 如果对你有帮助,记得点赞收藏哦!❤❤❤

相关推荐
Leyla8 分钟前
【代码重构】好的重构与坏的重构
前端
影子落人间11 分钟前
已解决npm ERR! request to https://registry.npm.taobao.org/@vant%2farea-data failed
前端·npm·node.js
世俗ˊ35 分钟前
CSS入门笔记
前端·css·笔记
子非鱼92136 分钟前
【前端】ES6:Set与Map
前端·javascript·es6
6230_40 分钟前
git使用“保姆级”教程1——简介及配置项设置
前端·git·学习·html·web3·学习方法·改行学it
想退休的搬砖人1 小时前
vue选项式写法项目案例(购物车)
前端·javascript·vue.js
加勒比海涛1 小时前
HTML 揭秘:HTML 编码快速入门
前端·html
啥子花道1 小时前
Vue3.4 中 v-model 双向数据绑定新玩法详解
前端·javascript·vue.js
麒麟而非淇淋1 小时前
AJAX 入门 day3
前端·javascript·ajax
茶茶只知道学习1 小时前
通过鼠标移动来调整两个盒子的宽度(响应式)
前端·javascript·css