vue+高德地圖開發採坑持續記錄

原文地址:https://liubing.me/vue-gaode-...

整理了下日常地圖開發過程中遇到的問題及解決方法,供大家參考,文章將會持續更新。

地圖引入問題

網上搜索了一些資料,大部門都是index.html直接引入高德地圖的js文件,個人感覺沒有必要,畢竟地圖只是部分頁面需要使用,所以這種方法直接不考慮了。
然後又找到了一種地圖懶加載的方法,需要的時候按需引入地圖即可,
整理了下按需加載地圖的js,我們可以新建一個js文件,如loadMap.js,位置可以隨意,
這裏爲了引入方便直接放組件同級目錄了,代碼如下:

/**
 * 動態加載高德地圖
 *
 * @export
 * @param {*} key 高德地圖key
 * @param {*} plugins 高德地圖插件
 * @param {string} [v='1.4.14'] 高德地圖版本
 * @returns
 */
export default function loadMap (key, plugins, v = '1.4.14') {
  return new Promise(function (resolve, reject) {
    if (typeof AMap !== 'undefined') {
      // eslint-disable-next-line no-undef
      resolve(AMap)
      return true
    }
    window.onCallback = function () {
      // eslint-disable-next-line no-undef
      resolve(AMap)
    }
    let script = document.createElement('script')
    script.type = 'text/javascript'
    script.src = `https://webapi.amap.com/maps?v=${v}&key=${key}&plugin=${plugins}&callback=onCallback`
    script.onerror = reject
    document.head.appendChild(script)
  })
}

使用

需要用到的地方直接引用這個文件,再mounted的時候執行

loadMap(this.key, this.plugins, this.v)
  .then(AMap => {
      // 此處地圖就加載成功了,然後就可以使用`new AMap.Map`來實例化地圖了
    console.log('地圖加載成功!')
  })
  .catch(() => {
    console.log('地圖加載失敗!')
  })

完整代碼

爲了更好的體驗,這裏加了一個loading動畫,地圖加載成功complete後取消loading效果。

<template>
  <div class="map">
    <div id="GDMap"
      v-loading="loading">
    </div>
  </div>
</template>

<script>
import loadMap from './loadMap'
export default {
  data () {
    return {
      // 地圖實例
      GDMap: null,
      // 加載的一些插件
      // 更多參考:https://lbs.amap.com/api/javascript-api/guide/abc/plugins#plugins
      plugins: [
        'AMap.OverView',
        'AMap.MouseTool',
        'AMap.PolyEditor',
        'AMap.RectangleEditor',
        'AMap.PlaceSearch',
        'AMap.DistrictLayer',
        'AMap.CustomLayer'
      ],
      // key
      key: 'c5eac55551560531336988396dacbf53',
      // 地圖版本
      v: '1.4.14',
      loading: true
    }
  },
  mounted () {
    loadMap(this.key, this.plugins, this.v)
      .then(AMap => {
        this.GDMap = new AMap.Map('GDMap', {
          zoom: 11,
          center: [116.397428, 39.90923]
        })
        this.GDMap.on('complete', () => {
          this.loading = false
        })
      })
      .catch(() => {
        this.loading = false
        console.log('地圖加載失敗!')
      })
  }
}
</script>

<style>
#GDMap {
  width: 1200px;
  height: 500px;
    position: relative;
}
</style>

效果圖

demo18.gif

獲取多邊形編輯的點的問題

說的直白點,就是多邊形在拖動點編輯過程中知道是哪個點
看了下官網的相關文檔:https://lbs.amap.com/api/javascript-api/reference/plugin#AMap.PolyEditor
沒找到相關的api,無奈只能自己寫了。
大致思路就是多邊形在編輯過程中其他的點的座標是不變了,唯一變化的就是被編輯的點,在編輯過程中做個判斷即可找到這個點的索引。
多邊形在被編輯過程中是會觸發change事件,所以可以利用這個事件寫些判斷,
在拿到這個點的索引後我們就可以乾點其他事情了。

// 多邊形的path
let polygonPath = polygon.getPath()
// 索引
let index
// change事件
polygon.on('change', (ev) => {
  const curPath = ev.target.getPath()
  for (let i = 0; i < path.length; i++) {
    // 判斷一直在變化的點
    if (polygonPath[i].lng !== curPath[i].lng || polygonPath[i].lat !== curPath[i].lat) {
      index = i
      break
    }
  }
  polygonPath = JSON.parse(JSON.stringify(curPath))
  console.log('編輯點索引:', index)
})

完整代碼

<template>
  <div class="map">
    <div id="GDMap"
      v-loading="loading">
    </div>
  </div>
</template>

<script>
import loadMap from './loadMap'
export default {
  data () {
    return {
      // 地圖實例
      GDMap: null,
      // 加載的插件
      plugins: [
        'AMap.OverView',
        'AMap.MouseTool',
        'AMap.PolyEditor',
        'AMap.RectangleEditor',
        'AMap.PlaceSearch',
        'AMap.DistrictLayer',
        'AMap.CustomLayer'
      ],
      // key
      key: 'c5eac55551560531336988396dacbf53',
      // 地圖版本
      v: '1.4.14',
      loading: true
    }
  },
  mounted () {
    loadMap(this.key, this.plugins, this.v)
      .then(AMap => {
        this.GDMap = new AMap.Map('GDMap', {
          zoom: 11,
          center: [116.397428, 39.90923]
        })
        this.GDMap.on('complete', () => {
          this.loading = false
        })

        const path = [
          [116.403322, 39.920255],
          [116.410703, 39.897555],
          [116.402292, 39.892353],
          [116.389846, 39.891365]
        ]
        const polygon = new AMap.Polygon({
          path: path,
          strokeColor: '#FF33FF',
          strokeWeight: 6,
          strokeOpacity: 0.2,
          fillOpacity: 0.4,
          fillColor: '#1791fc',
          zIndex: 50
        })
        // 地圖添加多邊形
        this.GDMap.add(polygon)
        // 縮放地圖到合適的視野級別
        this.GDMap.setFitView([ polygon ])

        // 多邊形編輯實例
        const polyEditor = new AMap.PolyEditor(this.GDMap, polygon)
        // 開啓編輯
        polyEditor.open()

        // 多邊形的path
        let polygonPath = polygon.getPath()
        // 索引
        let index
        // change事件
        polygon.on('change', (ev) => {
          const curPath = ev.target.getPath()
          for (let i = 0; i < path.length; i++) {
            // 判斷一直在變化的點
            if (polygonPath[i].lng !== curPath[i].lng || polygonPath[i].lat !== curPath[i].lat) {
              index = i
              break
            }
          }
          polygonPath = JSON.parse(JSON.stringify(curPath))
          console.log('編輯點索引:', index)
        })
      })
      .catch(() => {
        this.loading = false
        console.log('地圖加載失敗!')
      })
  }
}
</script>

<style>
#GDMap {
  width: 1200px;
  height: 500px;
}
</style>

效果圖

demo19.gif

信息窗口使用問題

看了下官網信息窗口的相關例子,發現寫法都不太友好,例如以下幾種形式的寫法:
image.png

image.png
要是按照這種寫法,vue中相關事件及數據傳遞怎麼整,後期維護還很困難,所以這種形式直接被我pass掉了,自己寫一個類似的組件,放在地圖上不就行了吧,自由程度高,可定製化也高。

簡單的信息窗口組件

自定義了一個簡單的信息窗口組件InfoWindow,顯示出來後大致就長這個樣子,UI樣式可以自己定義,所以很方便。
image.png

組件代碼

<div id="GDMap">
  <info-window></info-window>
</div>
<template>
  <div class="info-window">
    <div class="top">
      <span class="title">標題</span>
      <span class="close">x</span>
    </div>
    <div class="content">
      我是窗口的內容
    </div>
  </div>
</template>

<script>
export default {

}
</script>

<style scoped>
.info-window {
  position: absolute;
  top: 50%;
  left: 50%;
  padding: 10px;
  min-width: 300px;
  border-radius: 5px;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
  background-color: #ffffff;
  z-index: 10;
  transform: translate(-50%, -50%);
}
.info-window::after {
  content: "◆";
  font-size: 36px;
  height: 24px;
  color: #ffffff;
  position: absolute;
  bottom: 0;
  left: 50%;
  transform: translateX(-50%);
}

.info-window .top {
  padding-bottom: 5px;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.info-window .top .title {
  font-size: 12px;
}

.info-window .top .close {
  width: 24px;
  font-size: 12px;
  cursor: pointer;
}
</style>

窗口位置

這是官網信息窗口在地圖拖拽過程中的表現,會發現一直固定在某一個點相對位置
demo20.gif

再看看我們寫的信息窗口組件,看到差異了吧,信息窗口被固定死了。
demo21.gif

通過審覈元素你會發現官方的信息窗口的位置隨着地圖的拖拽在實時變化。
demo22.gif

所以加下來我們要做的就是這個。
看了下文檔,找到了地圖的一個方法lngLatToContainer
官方給出的描述是:地圖經緯度座標轉爲地圖容器像素座標
實際上窗口信息在地圖上顯示靠的是一個座標點,有了這個座標點信息窗口才知道需要哪個位置進行顯示,
對於信息窗口組件來說這個座標點就是absolute定位中的topleft,對於地圖來講就是lnglat經緯度,
所以先定一個地圖的座標點,pos: [116.397428, 39.90923],該座標也是地圖初始化時候的中心點座標(center)
地圖拖動過程中會觸發mapmove事件,在該事件中,通過lngLatToContainer方法獲取到pos點在地圖容器的實時像素座標,然後實時改變組件的topleft即可達到效果

相關代碼

this.GDMap.on('mapmove', () => {
  let position = this.GDMap.lngLatToContainer(this.pos)
  this.$refs.infoWindow.$el.style.left = position.x + 'px'
  this.$refs.infoWindow.$el.style.top = position.y + 'px'
})

預覽

這樣就差不多和官方的信息窗口差不多了。
demo23.gif

代碼完善

接下來我們將代碼稍微完善以下,增加一個新功能,點擊地圖上的熱點的時候,出現信息窗口,顯示相應熱點的信息。

代碼

<template>
  <div class="map">
    <div id="GDMap"
      v-loading="loading">
      <info-window v-if="visible"
        :pos="position"
        :info="info"
        @close="visible = false">
      </info-window>
    </div>
  </div>
</template>

<script>
import loadMap from './loadMap'
import InfoWindow from './InfoWindow'
export default {
  components: {
    InfoWindow
  },
  data () {
    return {
      // 地圖實例
      GDMap: null,
      // 加載的插件
      plugins: [
        'AMap.OverView',
        'AMap.MouseTool',
        'AMap.PolyEditor',
        'AMap.RectangleEditor',
        'AMap.PlaceSearch',
        'AMap.DistrictLayer',
        'AMap.CustomLayer'
      ],
      // key
      key: 'c5eac55551560531336988396dacbf53',
      // 地圖版本
      v: '1.4.14',
      loading: true,
      pos: [116.397428, 39.90923],
      position: {},
      visible: false,
      info: {}
    }
  },
  mounted () {
    loadMap(this.key, this.plugins, this.v)
      .then(AMap => {
        this.GDMap = new AMap.Map('GDMap', {
          zoom: 11,
          center: [116.397428, 39.90923]
        })

        // 地圖加載完成事件
        this.GDMap.on('complete', () => {
          this.loading = false
        })

        // 熱點點擊事件
        this.GDMap.on('hotspotclick', ev => {
          console.log(ev)
          this.visible = true
          this.pos = [ev.lnglat.lng, ev.lnglat.lat]
          this.info = ev
        })
      })
      .catch(() => {
        this.loading = false
        console.log('地圖加載失敗!')
      })
  },
  methods: {
    mapmove () {
      this.position = this.GDMap.lngLatToContainer(this.pos)
    }
  },
  watch: {
    pos (newPos) {
      this.position = this.GDMap.lngLatToContainer(newPos)
    },
    visible (newVisible) {
      if (this.GDMap) {
        if (newVisible) {
          // 綁定地圖平移事件
          this.GDMap.on('mapmove', this.mapmove)
        } else {
          // 移除事件綁定
          this.GDMap.off('mapmove', this.mapmove)
        }
      }
    }
  }
}
</script>

<style>
#GDMap {
  width: 1200px;
  height: 500px;
  position: relative;
}
</style>

信息窗口組件

<template>
  <div class="info-window"
    ref="infoWindow">
    <div class="top">
      <span class="title">{{ info.name }}</span>
      <span class="close"
        @click="handleClose">x</span>
    </div>
    <div class="content">
      id:{{ info.id }}<br>
      type:{{ info.type }}<br>
      lnglat:{{ info.lnglat.lng }},{{ info.lnglat.lat }}<br>
    </div>
    <div class="footer">
      by: liubing.me
    </div>
  </div>
</template>

<script>
export default {
  props: {
    // 像素座標
    pos: Object,
    // 窗口信息
    info: Object,
    // 信息窗口偏移
    offset: {
      type: Object,
      default: () => {
        return {
          x: 0,
          y: -20
        }
      }
    }
  },
  methods: {
    handleClose () {
      this.$emit('close')
    }
  },
  watch: {
    pos: {
      handler (newPos) {
        if (newPos && newPos.x && newPos.y) {
          this.$nextTick(() => {
            const infoHeight = document.querySelector('.info-window').clientHeight
            this.$refs.infoWindow.style.left = newPos.x + this.offset.x + 'px'
            this.$refs.infoWindow.style.top = newPos.y - infoHeight / 2 + this.offset.y + 'px'
          })
        }
      },
      immediate: true
    }
  }
}

</script>

<style scoped>
.info-window {
  position: absolute;
  top: 50%;
  left: 50%;
  padding: 10px;
  min-width: 300px;
  border-radius: 5px;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
  background-color: #ffffff;
  z-index: 10;
  transform: translate(-50%, -50%);
}
.info-window::after {
  content: "◆";
  font-size: 36px;
  height: 24px;
  color: #ffffff;
  position: absolute;
  bottom: 0;
  left: 50%;
  transform: translateX(-50%);
}

.info-window .top {
  padding-bottom: 5px;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.info-window .top .title {
  font-size: 12px;
}

.info-window .top .close {
  width: 24px;
  font-size: 12px;
  cursor: pointer;
}
.info-window .content {
  text-align: left;
}
.info-window .footer {
  font-size: 12px;
  color: #cccccc;
  text-align: right;
}
</style>

最終預覽

最後,大家可以根據這個法子自定義出自己的信息窗口組件,而不用受高德地圖信息窗口的約束了。
demo24.gif

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章