Vue+Antd搭配百度地圖實現搜索定位等功能


前言

最近,在做vue項目的時候有做到選擇地址功能,而原項目中又引入了百度地圖,所以我就打算通過使用百度地圖來實現地址搜索功能啦。

本次教程可能過於啰嗦,所以這里先放上預覽地址供大家預覽——點我預覽,也可到文末直接下載代碼先自行體驗。。。

ps: 又因為百度地圖 1.2 以上需要 AK 密鑰,所以這里我直接使用 1.2 版本實現
ps: 😐1.x版本是不能支持https的,所以使用時請注意

簡單的說下實現的效果

因為我這邊做的是打卡的地址選擇,那么肯定要有搜索提示來選取地址啦,又因為是打卡,肯定的打卡的范圍選擇。為了用戶體驗,我們也要添加點擊地圖任意位置生辰對應的地址,也要可以拖拽標注來生成對應地址。

既然知道了功能點,那么我們就上效果圖吧 😁

baidu-map-demo效果圖

看到這,我們大概知道的功能點有:

  • 設置圖像標注並綁定拖拽標注結束后事件
  • 綁定點擊地圖任意點事件
  • 封裝逆地址解析函數,用於通過坐標點獲取詳細地址
  • 添加輸入提示來選取地址
  • 添加地圖覆蓋物(圓),用於標識我們選擇的范圍

看到這里,是不是也想躍躍欲試啦,所以,我們就開始寫我們的代碼吧

搭建項目

因為,用到了vue,所以我們肯定安裝vue-cli這個腳手架啦,又因為Vue3發布了正式版,所以這次我們的教程當然是使用Vue3進行開發啦,所以我們腳手架可能需要更新一下。

npm install -g @vue/cli
# OR
yarn global add @vue/cli

ps: 建議都更新下咯,避免無法創建 vue3 的項目

這里我們選擇默認的配置就好了,如圖:

vue3默認配置

若安裝緩慢報錯,可嘗試用 yarn 或別的鏡像源自行安裝:rm -rf node_modules && yarn install。

在漫長的等他,他安裝了我們的模板,從標題我們也知道,這里我們使用ant-design-vue啦,因為element-ui現在還沒有支持Vue3,而element-plus的文檔還是element-ui的,對我們十分不友好,支持的也不完善,所以我們這里直接使用ant-design-vue@2.x啦。

所以廢話不多說了,直接安裝依賴:

npm i --save ant-design-vue@next

安裝完后我們就可以在main.js配置下我們的ant-design-vue

import { createApp } from "vue";
import App from "./App.vue";
import Antd from "ant-design-vue";
import "ant-design-vue/dist/antd.css";
createApp(App).use(Antd).mount("#app");

ps:因為這里我們只是做個例子,所以我為了方便直接使用全局了

既然我們用了Vue3,我們就說說 Vue3 對比 Vue2 有什么更爽的點

Vue2 與 Vue3 的對比

  • TypeScript 支持更友好了,因為 Vue2 所有屬性都放在了 this 對象上,難以推倒組件的數據類型。

  • 同第一點,所有屬性都放在了 this 對象上,難以實現 TreeShaking

  • Template 終於支持多個根標簽了,不需要每次寫模板的時候都加上多余的根元素。

  • Composition Api,也是我們最聽到的新功能(如果你用過React Hooks,那一定對它不陌生,因為它和React Hooks十分類似),很多人也建議優先使用Composition Api來替代Mixins的寫法,好處如下:

    1. 相關邏輯可以集中,且更容易復用
    2. 不會因為莫名的變數或方法名找半天,然后發現在Mixins
    3. 減少this指向問題
    4. 解決組件內的命名沖突
    5. 隱式依賴得到解決,你可以直觀的看到消費組件所需要的變量
    6. 其它等等…
  • 其它等等…

組合式 API

既然我們說了這么多 Composition Api 的優點,那么我們該怎么使用他呢?在 Vue 組件中,提供了一個setup的組件選項,並充當合成 API 的入口點。

ps: 由於在執行 setup 時尚未創建組件實例,即在 created 之前,因此在 setup 選項中沒有 this。這意味着,除了 props 之外,你將無法訪問組件中聲明的任何屬性——本地狀態、計算屬性或方法。

使用setup函數是,他將接受兩個參數,分別是propscontext

Props

setup 函數中的第一個參數是 props。正如在一個標准組件中所期望的那樣,setup 函數中的 props 是響應式的,當傳入新的 prop 時,它將被更新。

ps: 因為 props 是響應式的,你不能使用 ES6 解構,因為它會消除 prop 的響應性

上下文

context是一個普通的JavaScript對象,它暴露三個組件的 property:attrsslotsemit

export default {
  setup(props, context) {
    // Attribute (非響應式對象)
    console.log(context.attrs);
    // 插槽 (非響應式對象)
    console.log(context.slots);
    // 觸發事件 (方法) 同以前的 this.$emit()
    console.log(context.emit);
  },
};

context是一個普通的JavaScript對象,也就是說,它不是響應式的,這意味着你可以安全地對context使用ES6解構。

export default {
  setup(props, { attrs, slots, emit }) {
    // ...
  },
};

😢 因為我們不是Vue3基礎入門,所以我這里就只講用到的幾個 API,另Vue3支持大多數Vue2的特性,所以我們用Vue2語法開發Vue3也是完全沒問題的(🤣 開玩笑的)

ref 函數

閑話就不多說了,先來了解以下Composition Api的魅力吧。

在 Vue 3.0 中,我們可以通過一個新的ref函數使任何響應式變量在任何地方起作用。

並且ref返回的是一個對象值,該對像只包含一個 value 屬性,且只有我們在setup函數進行訪問/修改的時候需要加.value,接下來我們就修改下HelloWorld組件,來實現一下選擇最喜愛的水果的小程序吧。

<template>
  <div>請選擇你最喜歡的水果</div>
  <div>
    <button v-for="(fruit, idx) in fruits" :key="fruit" @click="handleSelect(idx)">
      {{ fruit }}
    </button>
  </div>
  <div>你最喜歡的是【{{ select }}】</div>
</template>

<script>
import { ref } from "vue";
export default {
  setup() {
    const fruits = ref(["芒果", "榴蓮", "菠蘿"]);
    const select = ref("");
    const handleSelect = (idx) => {
      select.value = fruits.value[idx];
    };
    return {
      fruits,
      select,
      handleSelect,
    };
  },
};
</script>

這樣子,我們的這個小 demo 就是實現啦。看下我們的代碼,有發現了什么嗎?沒錯,我們使用setup之后,可以完全不需要 data 和 methods 屬性,並且我們可以在組件模板中使用多個根節點。

reactive 函數

看了上面的代碼,可以說沒什么章法可言,所有的變量和方法都混淆在一起,最不能忍受的就是在 setup 中要改變和讀取一個值的時候,還要加上 value。那么這里,我們就引入一個新的 Api reactive來優化我們的代碼吧。

reactive函數接收一個普通對象,返回一個響應式的數據對象。既然是普通對象,那么無論是變量、還是方法,都可以作為對象中的一個屬性來使用啦,那么我們就能優雅的修改我們的值,不用再通過.value修改我們的值啦,那么就通過reactive修改下我們的代碼吧。

<template>
  <div>請選擇你最喜歡的水果</div>
  <div>
    <button v-for="(fruit, idx) in data.fruits" :key="fruit" @click="data.handleSelect(idx)">
      {{ fruit }}
    </button>
  </div>
  <div>你最喜歡的是【{{ data.select }}】</div>
</template>

<script>
import { reactive } from "vue";
export default {
  setup() {
    const data = reactive({
      fruits: ["芒果", "榴蓮", "菠蘿"],
      select: "",
      handleSelect(idx) {
        data.select = data.fruits[idx];
      },
    });
    return {
      data,
    };
  },
};
</script>

toRefs 函數

雖然我們通過reactive優化了代碼,但是看着都需要data.也不是事啊,那么有沒有什么方法優化這個點呢?實際是有的,Vue3 提供了 toRefs(),將響應式對象轉換為普通對象,其中結果對象的每個 property 都是指向原始對象相應 property 的 ref

那么我們繼續優化我們的代碼吧。

<template>
  <div>請選擇你最喜歡的水果</div>
  <div>
    <button v-for="(fruit, idx) in fruits" :key="fruit" @click="handleSelect(idx)">
      {{ fruit }}
    </button>
  </div>
  <div>你最喜歡的是【{{ select }}】</div>
</template>

<script>
import { reactive, toRefs } from "vue";
export default {
  setup() {
    const data = reactive({
      fruits: ["芒果", "榴蓮", "菠蘿"],
      select: "",
      handleSelect(idx) {
        data.select = data.fruits[idx];
      },
    });
    return {
      ...toRefs(data),
    };
  },
};
</script>

watch 函數

watch函數與選項式 APIthis.$watch(以及相應的 watch 選項) 完全等效。watch需要偵聽特定的data源,並在單獨的回調函數中副作用。默認情況下,它是懶執行,即回調是僅在偵聽源發生更改時調用。

雖然這里的自己不需要使用watch和獲取真實的DOM,但我這里也講一下,便於后面例子的代碼編寫(生硬的轉折 🤣)。

Vue3 獲取真實 dom 元素也比較簡單,基本和往常一樣,大概分為三步:

  1. 和以前一樣,在標簽上寫上 ref 名稱
  2. 在 setup 中定義一個和標簽上 ref 名稱一樣的 Ref 的示例,並返回
  3. onMounted 就可以得到 ref 的 RefImpl 的對象,並通過.value 獲取
<template>
  <div>請選擇你最喜歡的水果</div>
  <div>
    <button v-for="(fruit, idx) in fruits" :key="fruit" @click="handleSelect(idx)">
      {{ fruit }}
    </button>
  </div>
  <!-- 1.和以前一樣,在標簽上寫上 ref 名稱-->
  <div ref="selectRef">你最喜歡的是【{{ select }}】</div>
</template>

<script>
import { ref, reactive, toRefs, watch } from "vue";
export default {
  setup() {
    // 2. 定義一個和標簽上 ref 名稱一樣的 Ref 實例
    const selectRef = ref(null);
    const data = reactive({
      fruits: ["芒果", "榴蓮", "菠蘿"],
      select: "",
      handleSelect(idx) {
        data.select = data.fruits[idx];
      },
    });
    watch(
      () => data.select,
      (val, preVal) => {
        // 得到一個 RefImpl 的對象, 通過 .value 訪問到真實DOM
        console.log(selectRef.value);
        console.log(val, preVal);
      }
    );
    return {
      ...toRefs(data),
      selectRef,
    };
  },
};
</script>

當然,watch還可以監聽多個源:

watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
  /* ... */
});

到這里,基本上前置知識都過得差不多了,可以開始編寫我們的代碼了

正式編寫代碼

通過前面學習的知識點我們大概了解了 Vue3 最基本的用法,那么就可以編寫我們的代碼了

清理下無用的代碼

vue-cli 生產的 Vue3 項目中,我們修改了HelloWorld用於學習了 Vue3 的基本 Api,實際上我們接下來的案例是不需要這些代碼的,所以我們打開App.vue,去掉部分無關代碼,並在components目錄新建MapDialog.vue文件,內容如下:

<template>
  <div>這是地圖彈窗</div>
</template>

<script>
export default {
  name: "MapDialog",
};
</script>

清理無用代碼后並導入MapDialog組件

<template>
  <map-dialog />
</template>

<script>
import MapDialog from "./components/MapDialog.vue";
export default {
  name: "App",
  components: {
    MapDialog,
  },
};
</script>

百度地圖基本使用

前文也說了,我之前項目是通過script標簽引入的,所以這里我們也是直接引入 js 庫

ps: 也可以通過 npm 安裝 vue-baidu-map 引入vue-baidu-map這個百度地圖組件

  1. 引入 js 庫

打開public/index.html,引入 js

<script type="text/javascript" src="https://api.map.baidu.com/api?v=1.2"></script>
  1. 編寫代碼
<template>
  <div id="map"></div>
</template>

<script>
import { onMounted } from "vue";
export default {
  name: "MapDialog",
  setup() {
    onMounted(() => {
      const { Map, Point } = BMap;
      const map = new Map("map");
      const point = new Point(116.404, 39.915);
      map.centerAndZoom(point, 16);
      map.enableScrollWheelZoom();
    });
  },
};
</script>

<style scoped>
#map {
  height: 400px;
}
</style>

寫到這里可能會出現下圖的一個錯誤:

eslint

因為我們選擇了默認模板,里面又包括了eslint而我們又引入了一個BMap的全局變量,eslint不認識它,所以會報BMap is not defined.這個錯誤。怎么解決呢?我們只需要告訴eslint,這是全局變量即可,打開package.json,添加如下配置:

{
  // ...
  "eslintConfig": {
    // ...
    "globals": {
      "BMap": true,
      "BMAP_STATUS_SUCCESS": true
    }
  }
  // ...
}

值得注意的點是:

  • 容器 div 需要使用 id
  • 容器 div 需要指定寬高

其余用法與 html 中編碼無異

編寫完這個代碼后,我們就可以在頁面看到百度地圖的雛形並且不會報錯了,接下來就可以開始書寫其他功能的代碼啦 O(∩_∩)O~~

先從簡單的開始入手

從前文的效果圖可以知道,我們是通過點擊選擇位置按鈕來彈出地圖的,這里我就不一步步編寫基本的ui了,直接上基礎代碼了

App.vue代碼如下

<template>
  您選擇的位置是:{{ place.address }}
  <a-button @click="toggleVisible">選擇位置</a-button>
  <map-dialog v-model:visible="visible" :point="place.point" :range="place.range" @confirm="handleConfirm" />
</template>

<script>
import { reactive, toRefs } from "vue";
import MapDialog from "./components/MapDialog.vue";
export default {
  name: "App",
  components: {
    MapDialog,
  },
  setup() {
    const data = reactive({
      place: {},
      visible: false,
      toggleVisible() {
        data.visible = !data.visible;
      },
      handleConfirm(place) {
        data.place = place;
      },
    });
    return {
      ...toRefs(data),
    };
  },
};
</script>

這里用了我們v-mode:visiblevisible對這個props進行了雙向綁定,實際上在 Vue2.x 的寫法中是通過:visible.sync修飾符來實現的

詳細了解,請參考這個鏈接

MapDialog.vue 基礎代碼如下:

<template>
  <a-modal
    :visible="visible"
    centered
    title="請選擇地址"
    cancelText="取消"
    okText="確定"
    @cancel="close"
    @ok="handleOk"
  >
    <a-form class="form" layout="inline" ref="mapForm" :model="form" :rules="rules">
      <a-form-item name="address">
        <a-auto-complete
          v-model:value="form.address"
          :options="addressSource"
          placeholder="請輸入你要搜索的地點"
          @search="handleQuery"
          @select="handleSelect"
          style="width: 360px"
        />
      </a-form-item>
      <a-form-item name="range">
        <a-select v-model:value="form.range" placeholder="請選擇范圍" @change="setRadius">
          <a-select-option v-for="range in ranges" :key="range">
            {{ range }}
          </a-select-option>
        </a-select>
      </a-form-item>
    </a-form>
    <div id="map"></div>
  </a-modal>
</template>

<script>
import { ref, reactive, toRefs, watch, nextTick } from "vue";
export default {
  name: "MapDialog",
  props: {
    visible: {
      type: Boolean,
      default: false,
    },
    range: {
      type: String,
      default: "300米",
    },
    point: {
      type: Object,
      default: () => ({ lng: 113.271429, lat: 23.135336 }),
    },
  },
  setup(props, { emit }) {
    const mapForm = ref(null);
    const formData = reactive({
      form: {
        address: "",
        range: props.range,
      },
      rules: {
        address: [
          {
            required: true,
            message: "請輸入你要搜索的地點",
            trigger: "blur",
          },
        ],
      },
      ranges: ["100米", "300米", "500米"],
      addressPoint: props.point,
      addressSource: [],
      setRadius() {},
      handleQuery() {},
      handleSelect() {},
      close() {
        emit("update:visible", false);
        mapForm.value.resetFields();
      },
      handleOk() {
        mapForm.value.validate().then(() => {
          emit("confirm", {
            address: formData.form.address,
            point: formData.addressPoint,
            range: formData.form.range,
          });
          emit("update:visible", false);
        });
      },
    });

    const { Map, Point } = BMap;

    // 地圖相關元素,因為可能在別的方法使用
    let map = null;

    // 初始化地圖
    function initMap() {
      // 防止dom還未渲染
      nextTick(() => {
        // 禁用地圖默認點擊彈框
        map = new Map("map", { enableMapClick: false });
        const { lng, lat } = formData.addressPoint;
        const point = new Point(lng, lat);
        map.centerAndZoom(point, 16);
        map.enableScrollWheelZoom();
      });
    }
    watch(
      () => props.visible,
      (visible) => {
        visible && initMap();
      }
    );
    return {
      mapForm,
      ...toRefs(formData),
    };
  },
};
</script>

<style scoped>
#map {
  height: 400px;
}
.form {
  height: 66px;
}
</style>

復制進去,基本上整個模子就出來了,接下來就是實現我們的功能了

設置圖像標注並綁定拖拽標注結束后事件

百度地圖提供了很多覆蓋物供我們很多覆蓋物的類,而我們這里使用Marker標注點,也就是我們效果圖所看到的小紅點,因為它可以比較形象的標注用戶看到的興趣點(就比如我們選中的地址)。

當然,它也可以自定義新的圖標,不過這不是我們這篇案例的重點,有興趣的可以參考標注、(自定義 Marker 圖標)[http://lbsyun.baidu.com/jsdemo.htm#eChangeMarkerIcon]

設置圖像標注並並綁定拖拽事件非常簡單,只需要下面幾行代碼:

// 導入Marker類
const { Map, Point, Marker } = BMap;
// 地圖相關元素,因為可能在別的方法使用
let map = null,
  marker = null;

// 初始化地圖
function initMap() {
  // 防止dom還未渲染
  nextTick(() => {
    // ...
    // 創建一個圖像標注實例,允許啟用拖拽Marker
    marker = new Marker(point, { enableDragging: true });
    map.addOverlay(marker);
    // 標注拖拽
    marker.addEventListener("dragend", ({ point }) => {
      console.log(point);
    });
  });
}

這樣你就可以在地圖上看到小紅點,並且可以拖拽小紅點啦,拖拽釋放后還會在瀏覽器打印出坐標點。

綁定點擊地圖任意點事件

既然實現拖拽標注結束后獲取坐標點,當然在地圖上選取任意點,我們也需要獲取該點的地址信息啦。

實現也十分簡單,代碼如下:

// 初始化地圖
function initMap() {
  // 防止dom還未渲染
  nextTick(() => {
    // ...
    // 地圖點擊
    map.addEventListener("click", ({ point }) => {
      getAddrByPoint(point);
    });
  });
}

添加地圖覆蓋物(圓)

因為我們需要選中返回,那么覆蓋物-圓就最符合我們的需求了,所以我們接下來添加一下吧

// 因為默認的圓太難看了,所以我修改了下樣式
const circleOptions = {
  strokeColor: "#18A65E",
  strokeWeight: 2,
  fillColor: "#18A65E",
  fillOpacity: "0.1",
};

export default {
  // ...
  setup(props, { emit }) {
    // ...
    const { Map, Point, Marker, Circle } = BMap;

    // 初始化地圖
    function initMap() {
      // 防止dom還未渲染
      nextTick(() => {
        // ...
        // 創建一個覆蓋物——圓
        circle = new BMap.Circle(point, parseInt(formData.form.range), circleOptions);
        // 添加覆蓋物
        map.addOverlay(circle);
      });
    }
    return {
      mapForm,
      ...toRefs(formData),
    };
  },
};

既然已經添加了圓,那么當我們改變了范圍的時候這個圓肯定也要跟着改變啦

const formData = reactive({
  // ...
  setRadius() {
    circle.setCenter(formData.addressPoint);
    circle.setRadius(parseInt(formData.form.range));
  },
  // ...
});

切換一下,看我們的圓是不是會變大和變小啦?

封裝逆地址解析函數,用於通過坐標點獲取詳細地址

寫到這里,我們已經獲取可以點擊地圖和拖拽獲取坐標點了,那么我們缺少什么呢?沒錯,就是缺少了個可以解析坐標點的方法。

參考地址逆解析,我們就可以封裝一個根據坐標點可以獲取到距離位置的方法了,同時也可以給地圖設置默認的地址了。

const { Map, Point, Marker, Circle, Geocoder } = BMap;
const geco = new Geocoder();

// 逆地址解析函數
function getAddrByPoint(point) {
  geco.getLocation(point, (res) => {
    formData.addressPoint = point;
    formData.form.address = res.address;
    formData.setRadius();
    map.panTo(point);
    marker.setPosition(point);
  });
}

// 初始化地圖
function initMap() {
  // 防止dom還未渲染
  nextTick(() => {
    // ...
    // 標注拖拽
    marker.addEventListener("dragend", ({ point }) => {
      getAddrByPoint(point);
    });

    // 地圖點擊
    map.addEventListener("click", ({ point }) => {
      getAddrByPoint(point);
    });
    // ...
    // 設置默認地址
    geco.getLocation(point, (res) => {
      formData.form.address = res.address;
    });
  });
}

添加輸入提示來選取地址

實現到現在,其實基本上功能都已經寫完了,就差一個搜索功能。而百度地圖提供的檢索功能有很多,這里我采用的是本地檢索,感興趣的可以看看他其他的檢索功能。

Antdd 的 AutoComplete 可以參考這個鏈接,這里就不做進一步地講解了。

主要用到了searchselect兩個事件回調。

const formData = reactive({
  // ...
  handleQuery(query) {
    if (!query) {
      formData.addressSource = [];
      return;
    }
    local.search(query);
  },
  handleSelect(item) {
    const { point } = formData.addressSource.find(({ value }) => value === item);
    formData.addressPoint = point;
    formData.setRadius();
    marker.setPosition(point);
    map.panTo(point);
  },
  // ...
});

const { Map, Point, Marker, Geocoder, LocalSearch } = BMap;
// 地圖相關元素,因為可能在別的方法使用
let map = null,
  marker = null,
  circle = null,
  local = null;
// 初始化地圖
function initMap() {
  // 防止dom還未渲染
  nextTick(() => {
    // ...
    // 創建本地檢索實例供search回調使用
    local = new LocalSearch(map, {
      onSearchComplete: (results) => {
        if (local.getStatus() == BMAP_STATUS_SUCCESS) {
          const res = [];
          for (var i = 0; i < results.getCurrentNumPois(); i++) {
            const { title, address } = results.getPoi(i);
            res.push({
              ...results.getPoi(i),
              value: `${title}(${address})`,
            });
          }
          formData.addressSource = res;
        }
      },
    });
  });
}

至此,我們就完成了所有的功能點啦 φ(* ̄ 0  ̄) 當然,其實好多沒有完善的點,就等着各位之后完善咯

gitee 地址,github 地址

參考鏈接

Ant Design of Vue

什么是組合式 API?

最后

雖然本文羅嗦了點,但還是感謝各位觀眾老爺的能看到最后 O(∩_∩)O 希望你能有所收獲 😁


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM