Vue Baidu Map组件封装:多边形组件和右键菜单

在Vue上进行开发,地图使用了百度提供的Vue Baidu Map。当前版本为v0.21.15。官方文档地址:
https://dafrok.github.io/vue-baidu-map/#/zh/index
开发需求:在百度地图上动态进行多边形的显示,添加,删除,修改。
具体流程:

  1. 在地图上点右键,弹出菜单:添加多边形。点击菜单项后,进入绘制模式。
  2. 在绘制模式下,每次鼠标点击百度地图,都会在图上添加一个点。多次添加的点构成一个多边形。
  3. 随着鼠标在地图上移动,多边形的最后一个点也随之改变。
  4. 点击右键,弹出菜单:添加结束。点击菜单项后,多边形绘制完成。
  5. 在多边形上点右键,弹出菜单:修改多边形,删除多边形。
  6. 选择修改多边形,则多边形进入修改模式,可用鼠标拖动其每一个点。
  7. 选择删除多边形,则会将多边形从地图中删除。
  8. 可添加多个多边形,每个多边形的菜单互不影响。

分析整个流程,可以得出要解决的关键点有以下几个:

  1. 为地图添加右键菜单。
  2. 为地图添加左键点击事件和mousemove事件。
  3. 在地图上显示多边形。
  4. 在多边形上添加右键菜单。
  5. 将功能封装为一个单独的vue。

关键点

Vue Baidu Map实际是百度官方对百度JavaScript API v2.0进行的封装,动态为document创建了一个<script>标签并下载JavaScript API v2.0,因此其核心功能依然是JavaScript API v2.0提供的,Vue Baidu Map只是一层壳而已。
Vue Baidu Map包括BaiduMap在内的组件都是异步加载,加载完成后会触发ready事件。ready事件传入的参数为{BMap, map},因此在ready中可以拿到BMap与实例化后的map对象。既然有了这两个对象,那么对地图的操作完全可以使用常规html页面中调用百度JavaScript API v2.0的方式。

为地图添加右键菜单

官方文档中明确给出了为地图添加右键菜单的方式,即使用BmContextMenu组件:

<baidu-map class="map" :center="center" :zoom="zoom">
    <bm-context-menu>
        <bm-context-menu-item :callback="addPolygon" text="添加多边形"></bm-context-menu-item>
    </bm-context-menu>
</baidu-map>

使用组件的方式非常简单明了。
除了组件的方式,也可以在BaiduMap的@ready事件中使用js的方式添加:

mapReady({BMap, map}) {
  var addPolygon = function() {}
  var menu = new BMap.ContextMenu()
  menu.addItem(new BMap.MenuItem('添加多边形', addPolygon))
  map.addContextMenu(menu)
}

添加后,在地图上点右键,菜单就会弹出。

为地图添加左键点击事件和mousemove事件

左键点击事件可以直接为BaiduMap绑定@click事件。
若使用js的方式,可以用addEventListener()来添加:

var clickCallback = function(e) {}
map.addEventListener('click', clickCallback)

mousemove事件同理:

var mousemoveCallback = function(e) {}
map.addEventListener('mousemove', mousemoveCallback)

在地图上显示多边形

在地图上显示多边形可以使用BmPolygon组件:

<baidu-map class="map" :center="center" :zoom="zoom">
    <bm-polygon
      v-for="(item, index) in polygons"
      :path="item.path"
      :editing="item.editing"
      :key="index">
    </bm-polygon>
</baidu-map>

其中polygons是在data中定义的一个数组,其每个数据的结构为:

{
  path: [], // 所有点
  editing: false, // 是否可编辑
}

这样就可以在地图上显示多个多边形了。

在多边形上添加右键菜单

在百度地图的定义中,多边形属于覆盖物(overlay)。为覆盖物添加右键菜单,js的方式为:

var editPolygon = function() {}
var deletePolygon = function() {}
var menu = new BMap.ContextMenu()
menu.addItem(new BMap.MenuItem('修改', editPolygon.bind(polygonObj)))
menu.addItem(new BMap.MenuItem('删除', deletePolygon.bind(polygonObj)))

polygonObj.addContextMenu(menu)

其中polygonObj为多边形对象。
尝试使用BmContextMenu来为多边形添加菜单:

<bm-polygon
  :path="path"
  :editing="editing"
  <bm-context-menu>
      <bm-context-menu-item :callback="editPolygon" text="修改"></bm-context-menu-item>
      <bm-context-menu-item :callback="deletePolygon" text="删除"></bm-context-menu-item>
  </bm-context-menu>
</bm-polygon>

实际却会报错。
这是因为BmPolygon组件本身并没有提供对BmContextMenu的支持。官方给出的覆盖物添加菜单的例子是BmMarker的:
https://dafrok.github.io/vue-baidu-map/#/zh/context-menu/menu
于是查看BmMarker的源码:

<template>
<div>
  <slot></slot>
</div>
</template>
<script>
...
</script>

再查看BmPolygon的源码:

<script>
...
</script>

可见,BmMarker预留了一个<slot>给可能塞进来的元素,而BmPolygon则没有。因此将BmMarker的<template>部分复制给BmPolygon,再次编译运行,发现右键点击BmPolygon,会弹出菜单了,且其回调函数可以正常响应。

更新问题

多边形需要动态绘制。此时出现了一个问题:若页面初始化时多边形的各个点就已经确定,那么多边形点右键会弹出菜单。然而,若多边形是动态添加的,则点右键不会弹出菜单。
当多边形动态添加或修改时,会构造一个全新的对象。该对象与之前的多边形是独立的。而菜单依然依附在之前的多边形对象上。于是当菜单进行reload时就会有问题。
于是考虑放弃使用BmContextMenu,改为当在多边形上点右键时,调用js API动态生成多边形的菜单。
所以现在的问题变为:为多边形添加右键点击事件。

为多边形添加右键点击事件

查询js的事件可知,鼠标右键点击事件名为rightclick。然而直接为BmPolygon添加@rightclick响应函数,并不会触发。于是可断定BmPolygon并没有触发该事件。
实际上,官方给出的js API的demo里,覆盖物本身并不监听这些事件,需要调用addEventListener()来为其注册事件。于是封装的BmPolygon没有触发该事件也就理所当然了。
查看BmPolygon的源码,大致结构为:

<script>
...
export default {
  name: 'BmPolygon',
  mixins: [commonMixin('overlay')],
  props: {...},
  watch: {...},
  methods: {
    load() {
      const { BMap, map, path, ... } = this
      const overlay = new BMap.Polygon(
        ...
      })
      this.originInstance = overlay
      map.addOverlay(overlay)
      bindEvents.call(this, overlay)
      ...
    }
  }
}
</script>

可以看到在load函数中调用js API来new了一个Polygon并添加给map对象,并将其保存在this.originInstance中。这个Polygon对象就是整个组件的核心对象。
于是,要为Polygon对象注册rightclick事件,就在这里对overlay调用addEventListener()即可:

load() {
  ...
  const that = this
  overlay.addEventListener('rightclick', function(e) {
    that.$emit('rightclick', e)
  })
}

注意需要同步为BmPolygon触发一个rightclick,这样通过@rightclick注册给BmPolygon的函数才能响应。
其中参数e与click的参数e格式相同,其e.currentTarget为当前覆盖物js对象。
右键事件注册成功,剩下的就是在右键事件中调用js API为当前多边形对象添加菜单了。
添加给多边形的菜单,只有当多边形更新时才会失效。若多边形的点一直没有变化,那么添加给它的菜单也会一直都能响应。考虑到这一点,将菜单对象保存在多边形对象中,并添加一个if判断:

onRightClick(e) {
  var polygonObj = e.currentTarget
  if(polygonObj.menu === undefined) {
      var menu = new BMap.ContextMenu() // BMap需自行保存
      menu.addItem(new BMap.MenuItem('修改', function() {
        ...
      }))
      polygonObj.addContextMenu(menu)
      // 将menu保存在多边形对象中
      polygonObj.menu = menu
      // 添加菜单后不会立即弹出,需额外触发一次右键点击事件
      polygonObj.dispatchEvent('rightclick', e)
  }
}

这样,只有当多边形更新时才会创建新的菜单。
需要注意的一点是,用户在多边形上点击右键,是希望看到弹出菜单。然而,多边形右键的触发顺序是:若有菜单,先弹出菜单→触发rightclick函数。因此,若多边形此时没有菜单,尽管会在rightclick中创建并添加,但由于菜单的显示流程在前,所以菜单是不会弹出来的。于是在最后额外触发了一次右键点击事件,这次事件触发后,由于菜单已有,所以会正常弹出;再次触发rightclick函数,因为polygonObj.menu也不为undefined了,所以什么也不做。
对于用户的感知就是,点击矩形区域,弹出了菜单。

封装

功能已经实现了,接下来要对整体功能进行封装。然而遇到了一个新的问题:
当把BmPolygon直接放在BaiduMap组件中时,显示一切正常:

<baidu-map class="map" :center="center" :zoom="zoom">
    <bm-polygon
      v-for="(item, index) in polygons"
      :path="item.path"
      :editing="item.editing"
      :key="index">
    </bm-polygon>
</baidu-map>

若将BmPolygon封装到一个PolygonEx.vue文件中,用<div>进行包裹,然后再引入到BaiduMap组件中时,多边形不显示了:

<template>
<div>
  <bm-polygon
    v-for="(item, index) in polygons"
    :path="item.path"
    :editing="item.editing"
    :key="index">
  </bm-polygon>
</div>  
</template>

考虑到封装的PolygonEx.vue类似一个overlay组件,于是分析了一下Vue Baidu Map的overlay组件写法:

<script>
import commonMixin from '../base/mixins/common.js'

export default {
  ...
  mixins: [commonMixin('overlay')],
  ...
  methods: {
    load () {
      const {BMap, map, $el, pane} = this
      ...
      const overlay = new XXX()
      this.originInstance = overlay
      map.addOverlay(overlay)
    }
  }
}
</script>

如上,其结构都有如下特点:

  1. 都混入了一个commonMixin。
  2. 都提供了一个load()函数。
    查看commonMixin源码,发现其ready函数为:
ready () {
  const $parent = getParent(this.$parent)
  const BMap = this.BMap = $parent.BMap
  const map = this.map = $parent.map
  this.load()
  this.$emit('ready', {
    BMap,
    map
  })
}

其中调用了this.load()。所以load()函数是必须实现的。
因此,要自己实现一个覆盖物组件PolygonEx.vue,只需要混入commonMixin,并提供load()函数即可。
按这个规则修改PolygonEx.vue,多边形可以正常显示了。

其他

上面已经解决了最主要的一些问题。还有一些其他的细节,例如令矩形随鼠标移动而动态修改最后一个点,矩形不同状态下弹出的菜单不同,多个矩形之间状态独立菜单互不影响,矩形数组改变则所有矩形重绘等,都是需要考虑的。具体可以参考下面的源码。

源码

在页面的index.vue所在目录下,添加一个components文件夹,在其下添加两个vue文件:Polygon.vue和PolygonEx.vue。

Polygon.Vue

<template>
  <div>
    <slot/>
  </div>
</template>
<script>
import commonMixin from 'vue-baidu-map/components/base/mixins/common.js'
import bindEvents from 'vue-baidu-map/components/base/bindEvent.js'
import { createPoint } from 'vue-baidu-map/components/base/factory.js'

export default {
  name: 'BmPolygon',
  mixins: [commonMixin('overlay')],
  props: {
    path: {
      type: Array,
      default() {
        return []
      }
    },
    strokeColor: {
      type: String,
      default: '#CC3333'
    },
    strokeWeight: {
      type: Number,
      default: 2
    },
    strokeOpacity: {
      type: Number,
      default: 0.7
    },
    strokeStyle: {
      type: String,
      default: 'solid'
    },
    fillColor: {
      type: String,
      default: '#00CCFF'
    },
    fillOpacity: {
      type: Number,
      default: 0.1
    },
    massClear: {
      type: Boolean,
      default: true
    },
    clicking: {
      type: Boolean,
      default: true
    },
    editing: {
      type: Boolean,
      default: false
    }
  },
  watch: {
    path: {
      handler(val, oldVal) {
        this.reload()
      },
      deep: true
    },
    strokeColor(val) {
      this.originInstance.setStrokeColor(val)
    },
    strokeOpacity(val) {
      this.originInstance.setStrokeOpacity(val)
    },
    strokeWeight(val) {
      this.originInstance.setStrokeWeight(val)
    },
    strokeStyle(val) {
      this.originInstance.setStrokeStyle(val)
    },
    fillColor(val) {
      this.originInstance.setFillColor(val)
    },
    fillOpacity(val) {
      this.originInstance.setFillOpacity(val)
    },
    editing(val) {
      val ? this.originInstance.enableEditing() : this.originInstance.disableEditing()
    },
    massClear(val) {
      val ? this.originInstance.enableMassClear() : this.originInstance.disableMassClear()
    },
    clicking(val) {
      this.reload()
    }
  },
  methods: {
    load() {
      const { BMap, map, path, strokeColor, strokeWeight, strokeOpacity, strokeStyle, fillColor, fillOpacity, editing, massClear, clicking } = this
      const overlay = new BMap.Polygon(path.map(item => createPoint(BMap, { lng: item.lng, lat: item.lat })), {
        strokeColor,
        strokeWeight,
        strokeOpacity,
        strokeStyle,
        fillColor,
        fillOpacity,
        // enableEditing: editing,
        enableMassClear: massClear,
        enableClicking: clicking
      })
      this.originInstance = overlay
      map.addOverlay(overlay)
      bindEvents.call(this, overlay)
      const that = this
      overlay.addEventListener('rightclick', function(e) {
        that.$emit('rightclick', e)
      })
      // 这里有一个诡异的bug,直接给 editing 赋值时会出现未知错误,因为使用下面的方法抹平。
      editing ? overlay.enableEditing() : overlay.disableEditing()
    }
  }
}
</script>

PolygonEx.Vue

<template>
  <div>
    <bm-polygon
      v-for="(item, index) in polygons"
      :path="item.path"
      :editing="item.editing"
      :key="index"
      @ready="onReady($event)"
      @rightclick="rightClick(index, $event)"/>
  </div>
</template>
<script>
  import commonMixin from 'vue-baidu-map/components/base/mixins/common.js'
  import BmPolygon from './Polygon'

  export default {
    name: 'BmPolygonEx',
    components: { BmPolygon },
    mixins: [commonMixin('overlay')],
    props: {
      polygonData: {
        type: Array,
        default() {
          return []
        }
      }
    },
    data() {
      return {
        polygons: []
      }
    },
    watch: {
      polygonData(newPolygonData, oldPolygonData) {
        this.reload()
      }
    },
    methods: {
      load() {
        // 由于mix调用,该函数必须存在
        const { BMap, map } = this
        // 防止重复添加菜单
        if (map.menu === undefined) {
          var that = this
          var { polygons } = this

          // 添加多边形
          var addPolygonCallback = function() {
            polygons.push({
              editing: false,
              state: 'pre_draw',
              path: []
            })
          }

          var menu = new BMap.ContextMenu()
          menu.addItem(new BMap.MenuItem('添加多边形', addPolygonCallback))
          map.addContextMenu(menu)
          // 暂存菜单
          map.menu = menu

          // 点击地图
          var clickCallback = function(e) {
            var polygon = polygons[polygons.length - 1]
            switch (polygon.state) {
              case 'pre_draw':
                polygon.path.push(e.point)
                polygon.state = 'draw'
                break
              case 'draw':
                polygon.path.push(e.point)
                break
              default:
                break
            }
          }
          // 鼠标移动
          var mousemoveCallback = function(e) {
            if (polygons.length > 0 && polygons[polygons.length - 1].state === 'draw') {
              var path = polygons[polygons.length - 1].path
              if (path.length === 1) {
                path.push(e.point)
              } else if (path.length > 1) {
                // 调用vue的$set修改数组,可触发更新
                that.$set(path, path.length - 1, e.point)
              }
            }
          }
          map.addEventListener('click', clickCallback)
          map.addEventListener('mousemove', mousemoveCallback)
        }
        var polygons = []
        if (this.polygonData.length > 0) {
          this.polygonData.forEach(function(item, index, array) {
            polygons.push({
              state: 'idle',
              editing: false,
              path: item.path,
              data: item
            })
          })
        }
        this.polygons = polygons
      },
      onReady({ BMap, map }) {
      },
      rightClick(index, e) {
        var polygonObj = e.currentTarget
        if (polygonObj.menu === undefined) {
          console.log('%c 菜单不存在,创建', 'color: #CC00FF;')
          var that = this
          var { polygons, BMap } = this
          var menus = []
          // 变更状态函数
          var changeState = function(newState) {
            polygons[index].state = newState
            if (polygonObj.menu !== undefined) {
              polygonObj.removeContextMenu(polygonObj.menu)
            }
            // 一旦状态变更,弹出的菜单也会变更。故将菜单置空,等显示菜单时重建
            polygonObj.menu = undefined
          }
          switch (polygons[index].state) {
            case 'idle':
              menus.push({
                title: '修改多边形',
                callback: function() {
                  console.log('%c 修改多边形', 'color: #CC00FF;')
                  polygons[index].editing = true
                  changeState('modify')
                }
              })
              menus.push({
                title: '删除多边形',
                callback: function() {
                  console.log('%c 删除多边形', 'color: #CC00FF;')
                  if (polygons.length > index) {
                    // 删除多边形需要先触发事件,否则数据删掉就无法返回了
                    that.$emit('delete', { ...polygons[index] })
                    polygons.splice(index, 1)
                  }
                }
              })
              break
            case 'draw':
              menus.push({
                title: '添加结束',
                callback: function() {
                  console.log('%c 添加结束', 'color: #CC00FF;')
                  changeState('idle')
                  // 移除最后一个点,该点由鼠标移动生成
                  polygons[polygons.length - 1].path.pop()
                  if (polygons[polygons.length - 1].path.length < 3) {
                    polygons.pop()
                  } else {
                    that.$emit('drawover', polygons[polygons.length - 1])
                  }
                }
              })
              break
            case 'modify':
              menus.push({
                title: '修改结束',
                callback: function() {
                  console.log('%c 修改结束', 'color: #CC00FF;')
                  changeState('idle')
                  polygons[index].editing = false
                  that.$set(polygons[index], 'path', polygonObj.getPath())
                  that.$emit('modifyover', polygons[index])
                }
              })
              break
            default:
              break
          }

          var menu = new BMap.ContextMenu()
          menus.forEach(function(item, index, array) {
            menu.addItem(new BMap.MenuItem(item.title, item.callback.bind(polygonObj)))
          })
          polygonObj.addContextMenu(menu)
          polygonObj.menu = menu

          // 添加菜单后不会立即弹出,需额外触发一次右键点击事件
          polygonObj.dispatchEvent('rightclick', e)
        }
      }
    }
  }
</script>

index.vue

<template>
  <div class="page-main">
    <div ref="test" class="test">
      <button @click="doClick">重置矩形区</button>
    </div>
    <baidu-map ref="bdMap" v-bind="mapOptions" class="map-container">
      <bm-polygon-ex
        :polygon-data="polygonData"
        @drawover="callbakAdd"
        @modifyover="callbakModify"
        @delete="callbakDelete"/>
    </baidu-map>
  </div>
</template>
<script>
import {
  BaiduMap,
  BmContextMenu,
  BmContextMenuItem
} from 'vue-baidu-map/components'
import BmPolygonEx from './components/PolygonEx'

export default {
  components: { BaiduMap, BmPolygonEx, BmContextMenu, BmContextMenuItem },
  data() {
    return {
      mapOptions: {
        ak: 'xxxxxxxxxx',// 替换为自己的ak
        center: {
          lng: 116.405994,
          lat: 39.914935
        },
        scrollWheelZoom: true,
        zoom: 16
      },
      polygonData: [{
        path: [{ 'lng': 116.389932, 'lat': 39.924204 }, { 'lng': 116.395538, 'lat': 39.920856 }, { 'lng': 116.395142, 'lat': 39.915544 }, { 'lng': 116.389609, 'lat': 39.918089 }],
        value: 0
      }]
    }
  },
  methods: {
    doClick() {
      console.log('%c doClick', 'color: blue;')
      this.polygonData = [{
        path: [{ 'lng': 116.389932, 'lat': 39.924204 }, { 'lng': 116.395538, 'lat': 39.920856 }, { 'lng': 116.395142, 'lat': 39.915544 }, { 'lng': 116.389609, 'lat': 39.918089 }],
        value: 0
      }]
    },
    callbakAdd(e) {
      console.log('%c map callbakAdd', 'color: green;')
      console.log(e)
    },
    callbakModify(e) {
      console.log('%c map callbakModify', 'color: green;')
      console.log(e)
    },
    callbakDelete(e) {
      console.log('%c map callbakDelete', 'color: green;')
      console.log(e)
    }
  }
}
</script>
<style rel="stylesheet/scss" lang="scss" scoped>
  .page-main {
    width: 100%;
    height: 100%;
  }
  .map-container {
    width: 100%;
    height: 800px;
  }
</style>
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值