騰訊地圖開發填坑總結

2023-04-20 18:01:17

前言:騰訊地圖分為兩個版本,版本1是以Tmap為標誌,連結為https://map.qq.com/api/gljs?v=1.exp1為主, 版本2是以qq.map為標誌,連結為https://map.qq.com/api/gljs?v=2.exp2為主, 可以在匯入了線上連結的頁面上使用window.T或者window.q找到其地圖,這是由於在匯入了地圖連結後,地圖api將其掛載在了window上。 以版本1Tmap`為例:

所以寫騰訊地圖時要特別注意自己用的騰訊地圖的版本,版本一與版本二兩者之間有很大區別,在看api時也要注意字尾。

地圖api地址:

v1:
https://wemap.qq.com/Vis/JavascriptAPI/APIDoc/map
v2:
https://lbs.qq.com/webApi/javascriptV2/jsGuide/jsOverview

騰訊地圖放置在彈窗元件上的表現

筆者在最開始使用的是v1版本,但是Modal元件會顯示不正常的地圖,後來嘗試使用v2版本,結果發現Modal元件內根本無法回顯地圖,無奈之下只好選擇了v1版本,通過Modal元件本身的一些設定,實現了需求,另外,一些部落格上的關於使用騰訊地圖在彈窗上的實現,其彈窗都是自己實現的,程式碼不全,因而參考意義並不大。

貼貼,請留意Modal自身的一些設定:

<Modal centered zIndex={1001} onCancel={() => closeMapModal && closeMapModal()} forceRender={true} getContainer={document.body} open={MapModalOpen} title='選擇位置' footer={false} width={1220}>
 <div>
  <div id='container' ref={mapRef} style={{ width: '100%', height: '500px', position: 'relative' }} />
 <div>
</Modal>

然後就來到了坑2:

window上已掛載地圖,但是提示找不到地圖

這是由於Modal元件的緣故。Modal元件本身是可以有多次銷燬和顯示的,因而,假設要生成map範例,要在生成範例外使用settiemout(fn)包裹(讀者也可以使用PromiseVue開發者可以使用$nextTick,總之,不能使用同步的方式生成map範例)。

依然貼貼:

const initMap = useCallback(()=>{
// react中沒有nextTick,所以用了`setTimeout`
if(!isOpen)return 
if(!mapRef.current)return 
// 初始預設點
let tarLat = 39.984120
let tarLng = 116.307484
let initLatlng = new TMap.LatLng(tarLat, tarLng)
let myOptions = {
  zoom: 12,
  center: initLatlng,
  offset: { // 中心點偏移
    x: 0,
    y: 100,
  }
}
setTimeout(()=>{
   // 引數1:容器範例,設定項
  map = new TMap.Map(mapRef.current, myOptions)
})
},[isOpen])
關鍵字搜尋

v1中要想實現關鍵字搜尋功能,可以使用Suggestion類,想要使用Suggestion類需要額外匯入service此附加庫(注意,這個連結是替換動作,不是新增動作):

<script charset="utf-8" src="https://map.qq.com/api/gljs?v=1.exp&libraries=service&key=YOUR_KEY"></script>

// 外部必須要有setTimeout包裹,防止意外報錯
setTimeout(()=>{
// 如果不確認,可以在`new TMap`上檢視是否有service這個物件
  let suggest = new TMap.service.Suggestion({
    // 新建一個關鍵字輸入提示類
    pageSize: 20, // 返回結果每頁條目數
    region: '', // 限制城市範圍
    regionFix: false, // 搜尋無結果時是否固定在當前城市
  })
    // keyword:關鍵詞   location :使用者經緯度
   suggest.getSuggestions({ keyword: keyword, location: map.getCenter()}).then(res=>{
      if (res.status === 0 && Array.isArray(res?.data) && res?.data.length > 0) {
         // 這裡就拿到了資料
      }
  }) 
})

// res.data 子項範例,這是我們接下來操作的基礎:
{
    "id": "8672089425561259711",  // 這裡的id要記住,打點時要用到
    "title": "山東大學(洪家樓校區)",
    "address": "山東省濟南市歷城區洪家樓5號",
    "category": "教育學校:大學",
    "type": 0,
    "_distance": 2429,
    "location": {
        "lat": 36.687334,
        "lng": 117.068511,
        "height": 0
    },
    "adcode": 370112,
    "province": "山東省",
    "city": "濟南市",
    "district": "歷城區"
}

打點與批次打點功能

打點與批次打點,本質上是沒什麼區別的。都是通過範例化MultiMarker類來實現。為了避免頻繁生成marker範例而造成效能損耗,我們可以在地圖範例生成後先初始化marker範例,然後再在每次需要打點前先將上一次的marker資料清空再生成本次的marker
因而,單個打點與批次打點的區別僅僅在於內部的geometries的長度

剛剛講到初始化時要先範例化marker物件,其實 我們也應該將marker在最初時就定義,這個動作有點類似在vue的data中先賦初值

貼貼:

let marker
function MyMap(){
  const initMap = useCallBack(()=>{
   setTimeout(()=>{
    if(!isOpen) return
    // 此處為初始化地圖 略
    // do  init map 
    // 此處為初始化地圖 略
            // 初始化marker
        marker = new TMap.MultiMarker({
          map: map,
          styles: {
            // `default`欄位指的是marker的樣式名,也可以寫多個,也可以寫別的
            default: new TMap.MarkerStyle({
              // 點標註的相關樣式
              width: 34, // 寬度
              height: 46, // 高度
              anchor: { x: 17, y: 23 }, // 標註點圖片的錨點位置
              src: pointImg,     // 標註點圖片url或base64地址,不填則預設
              color: '#ccc', // 標註點文字顏色
              size: 16, // 標註點文字文字大小
              direction: 'center', // 標註點文字文字相對於標註點圖片的方位
              offset: { x: 0, y: -4 }, // 標註點文字文字基於direction方位的偏移屬性
              strokeColor: '#fff', // 標註點文字描邊顏色
              strokeWidth: 2, // 標註點文字描邊寬度
            }),
          },
          geometries: [] // 這裡就是地圖上點的數量
        })
  }) 
  },[isOpen])
}

不管是單個打點還是批次打點,筆者這邊的需求是打新的點前要先將上一次的點全部清空,因而,我們要先學習一下如何刪除點

// 新增點之前,先刪除點
// 此處拿得到marker範例是由於在init時就將new TMap.MultiMarker賦給了marker
// marker.getGeometries : 獲取當前marker物件有多少個點
// marker.remove([]) //remove方法用於刪除marker,根據內部的id,注意remove方法的引數是陣列格式
if (marker.getGeometries().length > 0) {
    // 全部刪除,也可以使用remove([id])進行單個刪除
    marker.remove(marker.getGeometries().map(v => v.id))
}

如果是以同一個marker物件下打點倒不用寫太多,甚至還想封裝一個函數...

  /**
   * markerMatter : marker物料資料,基於isBat    , 
   * list : 獲取到的總資料 
   * isBat是否批次,true則markerMatter為陣列態,false則為物件態
  */
const setMarker = (markerMatter, list, isBat = false)=>{
  if (marker.getGeometries().length > 0) {
      marker.remove(marker.getGeometries().map(v => v.id))
  }
// 根據是否批次的標識isBat來判斷並生成geoList待傳陣列,geoList本質上就是marker中的geometries,本質上就是替換動作
  const geoList = isBat ? list.map((item, i) => {
  return ({
    id: item.id,
    styleId: 'marker',
    position: new TMap.LatLng(item.location.lat, item.location.lng),
    content: i + 1 + '',  // 需求:批次打點時,要拿到搜尋的index項並標在marker上,但是單個的不需要。
    properties: {
      title: item.title
    }
  })
  }) : [{
  "id": markerMatter.id, // 非常重要,不填則刪不掉marker
  "styleId": 'marker',
  "position": new TMap.LatLng(markerMatter.location.lat, markerMatter.location.lng),
  content: undefined, // 單個不需要填充索引內容
  properties: {
    title: markerMatter.title
  }
  }]
  // !!!關鍵程式碼
  marker.add(geoList)
}
地圖上的彈窗與自定義彈窗與實現自定義彈窗點選事件

彈窗與marker總是成對存在。
之前的需求是不同的marker上顯示不同的windowinfo,需要多個windowinfo存在,後來需求有變,就成了點選哪個搜尋項或者點選哪個marker,對應的彈窗windwoinfo就回顯出來,本質上渲染的還是同一個windowinfo物件,所以我們依然可以使用同一個windowinfo來實現此功能,同marker

// 首先依然要先外部定義infoWindow
let infoWindow
function MyMap(){
const initMap = () =>{
  // 初始化地圖與範例化marker不再贅述
  // 初始化地圖與範例化marker不再贅述
    //  初始化info
    info = new TMap.InfoWindow({
      map: map,
      enableCustom: true,
      offset: { x: 0, y: -22 },// 設定偏移防止蓋住錨點
      position: new TMap.LatLng(tarLat, tarLng)  // 初始化
    })
    // 先呼叫一次關閉方法,防止出現多個
    info.close()
}
}

基於需求:整個地圖動作過程中,僅僅會出現一個infowindow,因而也就只需要一個就夠了
基於需求:彈窗上要新增按鈕,傳遞事件
需求1是很簡單的,但是需求2是很麻煩的,筆者翻遍了國內外各大知名不知名網站也沒找到解決辦法,原因在於,info.setContent方法(setContent:用於設定表單內容)僅接受字串格式,可以使用模板字串,但是一般不能傳遞事件。

甚至又想封裝個函數

想了想還是要先把一些坑點說一下,讀者可以根據筆者的思路來做,也可以另闢蹊徑

  // 設定表單資訊
// currentContent:當前選中的marker或者搜尋項,格式就是剛剛說的搜尋項的格式,不表;
// list: 就是總列表
  const setWindowInfo = (currentContent, list) => {
    // 首先先執行一次關閉方法,這是為了防止可能出現的彈窗異常。
    info.close()
    // 這裡是設定表單的內容,重點是要看下那個btn事件,它不能接受Antd的Button元件,但原生button的樣式又實在太醜,所以筆者直接將官網上Button元件的基礎樣式抄了下來,至於hover的樣式。。暫時不知道怎麼實現
    // 
    info.setContent(`
    <div style="position:relative">
    <div style="text-align:left">${currentContent.title}</div>
    <div style="text-align:left">${currentContent.address}</div>
    <span><label>經度:</label> <span>${currentContent.location.lat}</span></span>
    <span><label>緯度:</label><span>${currentContent.location.lng}</span></span>
    <button  style= "color: #fff;  background-color: #1677ff; box-shadow: 0 2px 0 rgba(5, 145, 255, 0.1);cursor:pointer;height: 32px;
    padding: 4px 15px;    font-size: 14px;border:none;
    border-radius: 6px;"  data-id=${currentContent.id}  id='btn'>新增</button>
  </div>
    `)
    // 設定定位
    info.setPosition(new TMap.LatLng(currentContent.location.lat,         
    currentContent.location.lng))
    // 設定好內容和位置後,我們將它開啟    
    info.open()
    /*
      好的,到這裡,我們要仔細講一下原理了,原理是通過HTML5的自定義屬性,也就是data-xx的方式去給她設定唯一值,通過這個唯一值我們可以得知使用者究竟在哪個marker上點選了這個位置,換句話說,只要我們在點選時確定了這個唯一值,button的功能也就實現了
    **/
     // 原生dom方式拿到這個dom
    let useLocationDom = document.getElementById('btn')
    // 繫結事件
    useLocationDom.addEventListener('click', function (e) {
      //  `dom.getAttribute`獲取繫結到data-xx的上的自定義屬性值
      const checkedOpt = list.find(v => v.id === useLocationDom.getAttribute('data-id'))
       // 業務邏輯,不表  
       //  你可以在這裡做任何你想做的...
      //  業務邏輯,不表 
      message.success(`新增成功`)
      // 此處用於功能完成後的一些樣式的優化
      useLocationDom.disabled = true
      useLocationDom.style.cursor = 'not-allowed'
      useLocationDom.style.color = 'rgba(0, 0, 0, 0.25)'
      useLocationDom.style.backgroundColor = 'rgba(0, 0, 0, 0.04)'

    })
  }

至於在搜尋項上點選使對應的點回顯彈窗,太過簡單,不表。

直接將選中項item跟總列表塞到這個setWindowInfo裡就好了啊喂!

大概寫了這些,總的來說地圖還是蠻簡單,看著api一把嗦就完事了。

以上。