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 希望你能有所收获 😁

posted @ 2020-11-03 12:31  磨蹭先生  阅读(3265)  评论(12编辑  收藏  举报