最终效果

显示效果
显示效果

点击SVG中节点高亮效果
点击SVG中节点高亮效果

在SVG中插入自定义元素整体效果
在SVG中插入自定义元素

需求描述:

  • 在页面中嵌入SVG文件,并解析。
  • 点击SVG文件中节点时,高亮显示。
  • 点击某些节点时,在页面中根据后端返回的坐标,框出位置信息。

解决方法:

参考文件:

GitHub - bumbu/svg-pan-zoom:JavaScript 库,支持在 HTML 文档中平移和缩放 SVG,使用鼠标事件或自定义 JavaScript 钩子

一. 在页面中嵌入SVG文件,并解析。

  • SVG文件是由后端返回的地址引入的。
  • 使用的 svg-pan-zoom 插件,它提供了对SVG图像的简单操作。

基础代码:

安装插件: npm install --save svg-pan-zoom

引用插件: import svgPanZoom from 'svg-pan-zoom'

<template>
  <div class="svg-box">
    <div class="svg-dialog__header">
      <!-- 标题部分 -->
      <span class="svg-dialog__title"> {{ SVGTitle }}</span>
      <button class="svg-dialog__headerBtn" @click="$emit('SVGClose')" >
        <i class="el-icon-close svg-dialog__close"></i>
      </button>
    </div>
    <div id="svg-dialog__body" v-loading="embedLoading">
      <!-- SVG嵌入部分 -->
      <embed type="image/svg+xml" id="svg-trigger" :src="currentSvg"/>
    </div>
  </div>
</template>
<script>
import svgPanZoom from 'svg-pan-zoom'
var panZoomTiger = null
var svgCon = null
var svgTiger = null
var svgDoc = null
export default {
  name: 'SVGDialog',
  data() {
    return {
      embedLoading: false, // 加载
      SVGTitle: '', // 窗口标题
      currentSvg: null, // SVG地址
      oldTargetNode: [], // 记录点击的节点
      jumpSVGData: {} // 记录跳转的数据
    }
  },
  created() {
    this.getData() // 获取窗口数据,显示SVG
  },
  methods: {
    // 获取窗口数据,显示SVG
    getData () {
      // 这里根据后端的接口获取了SVG的地址,赋值给了currentSvg
      .......... (接口赋值方法代码,无关,不展示了)
      // 赋值后调用展示SVG方法
      this.getZoomTiger()
    },
    // 获取SVG窗口
    // 跳转时addEventListener必须移除load
    getZoomTiger() {
      panZoomTiger = document.getElementById('svg-trigger')
      panZoomTiger.addEventListener('load', this.waitLoad)
    },
    // 定义svgPanZoom
    // 跳转时addEventListener必须移除click
    waitLoad() {
      svgTiger = svgPanZoom('#svg-trigger', {
        viewportSelector: '.svg-pan-zoom_viewport',
        preventMouseEventsDefault: false
      })
      svgCon = panZoomTiger.getSVGDocument().querySelector('svg')
      svgCon.style = 'cursor: pointer;'
      svgCon.addEventListener('click', this.svgClick)
    }
  },
</script>
<style scoped lang="scss">
  .svg-box {
    width: 100%;
    height: 100%;
  }
  .svg-dialog__header {
    padding: 20px 20px 10px;
    // border-bottom: 1px solid #909399;

    .svg-dialog__title {
      line-height: 24px;
      font-size: 18px;
      color: #303133;
    }

    .svg-dialog__headerBtn {
      position: absolute;
      width: 18px;
      height: 18px;
      top: 20px;
      right: 20px;
      padding: 0;
      background: transparent;
      border: none;
      outline: none;
      cursor: pointer;
      font-size: 18px;

      .svg-dialog__close {
        color: #909399;
        // color: black;
        &:hover {
          color: #409EFF!important;
        }
      }
    }
  }

  #svg-dialog__body {
    height: calc(100% - 54px);

    #svg-trigger {
      width: 100%;
      height: 100%;
    }
  }
</style>

页面基本就搭建好了,这里主要是在<embed>标签中展示的。

  •  <embed> :定义了一个容器,用来嵌入外部应用或者互动程序。标签支持 HTML 的全局属性和事件属性。
  • 代码中多个方法也主要是因为addEventListener事件监听。
  • viewportSelector:指定自己的视口选择器。

关于svgPanZoom的其他配置,可以在参考文档中查看更多。

 此时页面应该可以看到SVG文件。

后台查看显示
后台查看时显示的代码,SVG文件已经成功引入

二. 点击SVG文件中节点时,高亮显示。

 在基础代码中添加高亮方法的代码:

    // 点击SVG节点操作
    svgClick (evt) {
      var that_ = this
      let newObj = []
      if (evt.target.nodeName === 'text') {
        // 判断点击节点是否存在path
        if (evt.target.parentNode.children[0].nodeName !== 'text') {
          newObj.push(evt.target)
        } else if (evt.target.parentNode.children[0].nodeName === 'text') {
          newObj = evt.target.parentNode.children
        }
        // 记录点击的节点
        // 用于下次点击时取消上次点击节点的高亮
        if (that_.oldTargetNode.length === 0) {
          // 最开始记录
          that_.$set(that_, 'oldTargetNode', newObj)
        } else if (that_.oldTargetNode.length > 0) {
          // 高亮过取消,重新记录
          that_.resetHightLight(newObj)
        }
        // 高亮当前点击
        that_.hightLight(newObj, false)
      }
    },
    // 高亮SVG节点
    hightLight(ids, flag) {
      // let targetCon
      for (let i = 0; i < ids.length; i++) {
        ids[i].style.fill = 'rgb(255,0,0)'
        ids[i].style.stroke = 'rgb(255,0,0)'
        ids[i].style.strokeWidth = 0.3
        // if (i === 0) targetCon = ids[i].getBoundingClientRect()
      }
      // 可有可无,看自己个人感觉无效果,但也不影响代码和页面就先放着了
      // if (flag) {
      //  setTimeout(() => {
      //    svgTiger.zoomAtPoint(1.5, {x: targetCon.x, y: targetCon.y})
      //  }, 100)
      }
    },
    // 取消高亮
    resetHightLight(ids) {
      for (let i = 0; i < this.oldTargetNode.length; i++) {
        this.oldTargetNode[i].style.fill = 'rgb(0, 0, 0)'
        this.oldTargetNode[i].style.stroke = 'rgb(0, 0, 0)'
        this.oldTargetNode[i].style.strokeWidth = 0
      }
      this.$set(this, 'oldTargetNode', ids)
    }
  • 首先判断所点击的是否是text,再判断和text同一级的兄弟元素是否包含path。 
  • evt.target返回所点击的信息,evt.target.nodeName返回所点击位置的标签名称,有:path、text、svg等。
  • evt.target.parentNode.children[0].nodeName:根据经验及多次点击的结果验证,如果同一级别的兄弟元素包含path,那第一个元素的nodeName就是path,所以这里只判断了第一个兄弟元素,没有循环查找。
  • oldTargetNode:记录上一次点击的元素,如果兄弟元素包含path,就只记录所点击的text元素,如果不包含则记录同一级的所有兄弟元素。第一次点击记录第一次,往后每次点击都记录上一次。主要用于高亮节点后再次点击时,取消上次高亮的节点。
  • fill:填充色,可以理解为text的颜色。

  • stroke:轮廓颜色,可以理解为border。

  • strokeWidth:轮廓宽度/厚度。

三. 点击某些节点时,在页面中根据后端返回的坐标,框出位置信息。

  • 点击某些特定的节点时,发送接口,根据接口返回的坐标信息,在SVG中动态插入元素。
  • 后端接口返回的是与SVG中的transform数据相吻合的数据。
  • 如:H6,返回的就是transform="matrix(1 0 0 -1 66.0264205932617 210.158630371094)",如图所示,取坐标及算法返回。
H取横坐标,6取纵坐标

 在基础代码中添加高亮方法的代码:

    // 在SVG中创建并加入新的元素
    // full: matrix中前四位算法(后端返回)
    // X:横坐标(后端返回)
    // Y:纵坐标(后端返回)
    addSVGElement(full, x, y) {
      svgDoc = panZoomTiger.getSVGDocument().getElementsByClassName("svg-pan-zoom_viewport")[0].getElementsByTagName("g")[0]
      const newRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect') // 创建一个svg元素
      const tMatrix = `matrix(${full} ${x} ${y})`
      newRect.setAttribute('transform', tMatrix)
      newRect.setAttribute('width', '30')
      newRect.setAttribute('height', '30')
      newRect.setAttribute('fill', 'rgb(0,0,0)')
      newRect.setAttribute('fill-opacity', '0')
      newRect.setAttribute('stroke', 'rgb(255,0,0)')
      newRect.setAttribute('stroke-width', 1)
      newRect.setAttribute('x', -15) // 宽度的一半
      newRect.setAttribute('y', -15) // 高度的一半
      svgDoc.appendChild(newRect)
    },
  • 代码中没有写特定节点的筛选和发送接口后返回的信息,每个项目的触发条件不同,根据自己的适当调整然后调用该方法即可。参数full, x, y都是后端返回的信息。
  • full: matrix中前四位算法。x:横坐标。y:纵坐标。
  • rect:SVG 的一个基本形状,用来创建矩形元素,还可以用来创建圆角矩形。

  • newRect.setAttribute('transform', tMatrix)决定了这个元素的位置。

  • svgDoc:获取元素时,一定要找到真正的<g>标签。在浏览器解析时,在源文件的<svg>标签与<g>标签中,多创建了一层<g>标签。插入的层级不对,显示的位置就不对。

元素插入的位置
元素插入的正确位置

 如果需要移除创建的元素,代码:

    // 在SVG中移除某个元素元素
    removeSVGElement() {
      const removeRect = svgDoc.getElementsByTagName('rect')
      while (removeRect.length > 0) {
        const SVGRect = removeRect[0]
        removeRect[0].parentNode.removeChild(SVGRect)
      }
    }

四.根据搜索的内容,高亮SVG的节点(高亮与搜索内容匹配的第一个元素节点)

这个需求不算完善,后期完善需求时补充的。代码:

    // 获取搜索节点
    getSearchText(text) {
      const con = panZoomTiger.getSVGDocument().getElementsByClassName("svg-pan-zoom_viewport")[0].getElementsByTagName("g")[0].getElementsByTagName("g")[0]
      let objText = []
      const returnVal = this.recSearch(con.children, text)
      // 如果有就高亮
      if (returnVal) {
        objText.push(returnVal)
        this.oldTargetNode = objText
        this.hightLight(objText, false)
      }
    },
    // 循环查找,找到第一个匹配的则不继续查找
    recSearch(lists, text) {
      const hasChildrenAttr = function (obj) {
        return obj.children.length !== 0
      }
      for(let i = 0; i < lists.length; i++) {
        if (lists[i].innerHTML.trim().toLowerCase() === text.toLowerCase()) {
          return lists[i]
        } else if (hasChildrenAttr(lists[i])) {
          const result = this.recSearch(lists[i].children, text)
          if (result) {
            return result;
          }
        }
      }
      return null
    },
  • 一个节点可能是由多个标签组合而成的。
  • 搜索的内容如果不在一个标签中,则查找不到。
  • 查找到符合的节点后高亮显示,但下一次点击text时,则会取消本次高亮。

补充:对搜索节点高亮的同时并放大

评论区有一个小伙伴也提供了类似的方法,但使用transform存在直接改变原始文件种G标签transform值的问题,从而导致图纸放大后,后面有关于图纸显示、鼠标移动等操作也同时放大的问题,改进后方法如下:

    enlargeSVGEmbed(targetCon) {
      // 获取SVG窗口宽高
      const svgBoxWidth = this.$refs.svgDialog.offsetWidth // 窗口宽
      const svgBoxHeight = this.$refs.svgDialog.offsetHeight // 窗口高
      // 节点相对中心位置偏移度数
      const targetCenterHeight = (svgBoxHeight / 2) - targetCon.top - (targetCon.height / 2) * 3
      const targetCenterWidth = (svgBoxWidth / 2) - targetCon.left + targetCon.width * 3
      svgTiger.pan({ x: targetCenterWidth, y: targetCenterHeight })
      svgTiger.zoom(3)
    },

改的比较紧急,这个方法只是临时放大和移动,不会影响窗口显示区域和图纸原始值。

 总结:

从确定客户提出需求到实现后演示,用了22个小时完成,但之前从来没有接触过SVG这方面,所以做的不是很完美,比如搜索到的节点虽然高亮了,但没有放大显示。

后期会不断完善,再做补充~

Logo

前往低代码交流专区

更多推荐