在 Vue3 + TypeScript 项目中,为了采用 标签组件 的方式,使用百度地图组件,冲浪发现了一个开源库 ovo,很方便!喜欢的朋友记得帮 原作者 点下 star ~
目录
快速上手
全局注册
一次性引入 百度地图组件库 的所有组件
import { createApp } from 'vue'
import App from './App.vue'
import BaiduMap from 'vue-baidu-map-3x'
const app = createApp(App);
app.use(BaiduMap, {
// ak 是在百度地图开发者平台申请的密钥 详见 http://lbsyun.baidu.com/apiconsole/key */
ak: '百度地图ak',
// v:'2.0', // 默认使用3.0
// type: 'WebGL' // ||API 默认API (使用此模式 BMap=BMapGL)
});
app.mount('#app');
局部注册
<template>
<baidu-map class="map" ak="BaiduMapAK" v="3.0" type="API" :center="{lng: 116.404, lat: 39.915}" :zoom="15">
</baidu-map>
</template>
<script setup>
import { BaiduMap } from 'vue-baidu-map-3x'
</script>
<style>
.map {
width: 100%;
height: 300px;
}
</style>
注意事项
- 百度地图容器 必须 定义高度:BaiduMap 组件容器本身是一个空的块级元素,如果容器不定义高度,百度地图将渲染在一个高度为 0 不可见的容器内
- 百度地图容器 必须 定义 center、zoom:没有设置 center 和 zoom 属性的地图组件是不进行地图渲染的。当 center 属性为合法地名字符串时例外,因为百度地图会根据地名自动调整 zoom 的值
- 在 ready 中执行地图 API 加载后的代码,不能在 onMounted 中执行:由于百度地图 JS API 只有 JSONP 一种加载方式,因此 BaiduMap 组件及其所有子组件的渲染只能是异步的。因此,请使用在组件的 ready 事件来执行地图 API 加载完毕后才能执行的代码,不要试图在 vue 自身的生命周期中调用 BMap 类,更不要在这些时机修改 model 层
错误示例
<script setup>
import {ref,onMounted} from 'vue';
const center = ref({lng: 0, lat: 0});
const zoom = ref(3);
onMounted(() => {
center.value.lng = 116.404;
center.value.lat = 39.915;
zoom.value = 15;
});
</script>
正确示例
<script setup>
import {ref,onMounted} from 'vue';
const center = ref({lng: 0, lat: 0});
const zoom = ref(3);
const handler = ({BMap, map}) => {
console.log(BMap, map);
center.value.lng = 116.404;
center.value.lat = 39.915;
zoom.value = 15;
}
</script>
输入框搜索点位,并定位至该点位
实现效果
二次封装地图组件:
- 该组件可以被 编辑、查看、其他页面 进行引入
- 该组件可以接收指定的值(地点名称、经纬度等等)
- 点击 确认按钮 后,搜索显示结果列表
- 点击 结果列表的某项 后,定位到该点,并让该点居中展示
接收搜索字段,显示搜索结果列表:
点击结果列表,执行定位,展示点位信息:
引入地图组件
先来看看完整代码
<baidu-map
class="ths-map"
:zoom="mapZoom"
:center="mapCenter"
@ready="ready"
>
<!-- 比例尺控件 -->
<bm-scale anchor="BMAP_ANCHOR_TOP_RIGHT"></bm-scale>
<!-- 缩放控件 -->
<bm-navigation anchor="BMAP_ANCHOR_TOP_RIGHT"></bm-navigation>
<!-- 定位控件 -->
<bm-geolocation anchor="BMAP_ANCHOR_BOTTOM_RIGHT" :show-address-bar="true" :auto-location="true"></bm-geolocation>
<!-- 自定义控件 -->
<bm-control class="map-search-bm-control">
<!-- 手写输入框 -->
<div class="flex map-search-container">
<el-input v-model="keyword" class="map-search-input" @input="getSearchList"></el-input>
<el-button type="primary" class="map-search-btn" @click="getSearchList">
搜索
</el-button>
</div>
<!-- 搜索列表 -->
<div v-if="searchListVisible" class="map-search-list">
<div
v-for="(elem, eIndex) of searchList"
:key="eIndex"
class="map-search-item"
@click="performPositioning(elem)"
>
<p class="map-search-item-title">
{{ elem.name }}
</p>
<p class="map-search-item-addr">
{{ elem.address }}
</p>
</div>
</div>
</bm-control>
<!-- 点 https://map.heifahaizei.com/doc/overlay/marker.html -->
<bm-marker
v-if="isShowSearchPoint"
:position="{ lng: searchPoint.location.lng, lat: searchPoint.location.lat }"
@click="spInfoWindow.show = true"
>
</bm-marker>
<!-- 搜索点位弹窗 https://map.heifahaizei.com/doc/overlay/info-window.html#%E5%B1%9E%E6%80%A7 -->
<bm-info-window
:show="spInfoWindow.show"
:title="spInfoWindow.name"
:offset="{ width: 0, height: -24 }"
:position="{ lng: spInfoWindow.location.lng, lat: spInfoWindow.location.lat }"
:width="0"
:max-width="300"
@close="spInfoWindow.show = false"
@open="spInfoWindow.show = true"
>
<!-- {{ spInfoWindow }} -->
<!-- 搜索点位地址 -->
<p class="sp-info-window-addr">
{{ spInfoWindow.spName }}:<br />
{{ spInfoWindow.spAddress }}
</p>
<div class="sp-info-window-header">
<!-- 搜索点位名称 -->
<!-- <span class="sp-info-window-name">{{ spInfoWindow.spName }}</span> -->
<span class="sp-info-window-around" @click="infoWinGoAround"> 搜周边 </span>
</div>
</bm-info-window>
</baidu-map>
为什么不采用 BmAutoComplete?
这个需求使用 官网组件 bm-auto-complete 能实现,不采用是基于以下几个原因:
- UI 定制效果太差了,无法 深度定制 输入框样式(使用 el-input 组件会出现 bug,貌似只能采用原生 input 组件)
- 控制 请求发送时机 不方便(文字发生变化直接就请求了,有的时候希望点了确认按钮再请求)
- 无法自定义请求结果列表
使用 BmControl 实现自定义控件
为了解决上一个问题,采用了 BmControl
定义响应式变量
关于初始化变量的踩坑
这些变量 尽量 一开始就写好数据结构:
- 比如 mapCenter 里,一开始就应该定义 lng、lat,后面具体的值可以是 0 state.mapCenter = { lng: 0, lat: 0}
- 如果直接初始化成 state.mapCenter = {},会导致发生不可预知的错误
当然这并不是绝对的:
- 我最开始没初始化 单个点位 searchPoint 的对象结构时,没报错
- 我最开始没初始化 点位信息弹窗 spInfoWindow 的对象结构时,就报错找不到 lng 属性了
const state = reactive({
// 地图缩放级别
mapZoom: 11,
// 地图中心点
mapCenter: {
lng: 113.88402,
lat: 22.555259,
},
// 是否展示搜索点位
isShowSearchPoint: false,
// 搜索点位信息
searchPoint: {
lng: 0,
lat: 0,
} as any,
// 搜索点位弹窗信息
spInfoWindow: {
show: false,
spName: '',
spAddress: '',
location: {
lng: 0,
lat: 0,
},
} as any,
// 搜索关键字
keyword: '',
// 搜索列表
searchList: [] as any[],
// 搜索列表是否可见
searchListVisible: false,
});
初始化方法可以在 ready 函数中执行,不能在 onMounted 中执行
/**
* 页面初始化
*/
const initPage = () => {
// 是否展示搜索点位
state.isShowSearchPoint = false;
// 搜索点位信息
state.searchPoint = {} as any;
// 搜索点位弹窗信息
state.spInfoWindow = {
show: false,
spName: '',
spAddress: '',
location: {
lng: 0,
lat: 0,
},
};
// 搜索列表
state.searchList = [];
// 搜索列表是否可见
state.searchListVisible = false;
};
添加地图初始化方法 ready
监听外部组件传入地址的监变化
外部传入的地址,会被赋值给 state 中的变量,防止 props 被修改
watch(
() => props.complaintsAddr,
() => {
state.keyword = props.complaintsAddr;
// 关闭周边搜索弹框
state.aroundDialogVisible = false;
},
);
ready 方法逻辑
props.complaintsAddr 是供其他页面传入 地址 的字段:
- 如果接收到了这个字段,则直接调用百度地图 API 接口,获取搜索结果列表,执行定位
- 如果没接收到这个字段,则执行初始化,调整 state 中的变量,隐藏不该出现的内容
const ready = async (e: any) => {
if (props.complaintsAddr) {
await getSearchList();
for (const elem of state.searchList) {
if (elem.address === props.complaintsAddr || elem.name === props.complaintsAddr) {
performPositioning(elem);
break;
}
}
} else {
// 页面初始化
await initPage();
}
};
onMounted(async () => {
/*
* 页面初始化
* await initPage();
*/
});
获取地址搜索结果列表
使用百度地图 API 检索跨域
本地调用百度接口,会出现跨域问题,这是正常的,使用 nginx 代理本地发送的请求
location /mapapi/ {
proxy_pass http://api.map.baidu.com/;
add_header Access-Control-Allow-Origin *;
}
获取搜索结果列表方法
/**
* 获取搜索列表
*/
const getSearchList = async () => {
state.searchList = [];
try {
const res = await getInputList(state.keyword, '宝安区');
if (res.data.message === 'ok') {
state.searchList = res.data?.results;
// 展示搜索列表
state.searchListVisible = true;
}
} catch (err) {
console.error('地图 搜索列表 接口请求失败', err);
state.searchList = [];
state.searchListVisible = true;
}
};
执行定位
使用 nextTick 修改数据
单个点 BmMaker 组件,使用 v-if 进行控制,否则一开始没点搜索结果的时候,就会直接渲染
使用 nextTick,修改点位坐标数据、中心点数据、点位弹框信息,原因如下:
- 如果不使用 nextTick,会导致 点位弹框信息 报错 —— 获取不到 img 这个 DOM 元素
- 也就是说,点位弹框信息 必须在 点位存在 的时候,才能渲染
选择结果列表后,应该把选中的结果信息发出去,告诉外面使用地图的组件
/**
* 执行定位
* @param elem 点位信息,必传
* @param isCloseSearchList 是否关闭搜索列表,菲必传
*/
const performPositioning = async (elem: any) => {
// console.log('执行定位 ---', elem);
// 隐藏列表
state.searchListVisible = false;
// 修改关键字信息
state.keyword = elem.address ? elem.address : elem.name;
// 更新外部组件的关键字信息
emit('search-point-info', elem);
// 渲染点位
nextTick(() => {
// 修改中心点坐标,把当前搜索点,设置为中心点
state.mapCenter.lng = elem.location.lng;
state.mapCenter.lat = elem.location.lat;
// 修改搜索点位坐标
state.searchPoint = elem;
// 展示搜索点位
state.isShowSearchPoint = true;
console.log('搜索点位的经纬度 ---', state.searchPoint);
// 设置点位弹窗信息
showInfoWindow(elem);
});
};
点位弹窗信息
/**
* 展示点位弹窗
* @param elem 点位信息
*/
const showInfoWindow = (elem: any) => {
// 设置点位弹窗信息
state.spInfoWindow.spName = elem.name; // 标题
state.spInfoWindow.spAddress = elem.address; // 地址
state.spInfoWindow.location = elem.location; // 弹框坐标
// 展示点位信息弹框
state.spInfoWindow.show = true;
// 修改搜索点位坐标
state.searchPoint = elem;
state.mapCenter = elem.location;
console.log('展示点位弹窗 ---', state.spInfoWindow);
};
展示搜索点位的周边点位弹窗
实现效果
- 点击搜索点位的去周边后,出现弹框
- 弹窗只能在地图中显示,不能在整个屏幕中显示
- 搜索到周边点位后,地图绘制周边距离(辐射圆),并把所有点位进行打点
引入地图组件
辐射圆、海量点组件
<!-- 添加-多个点 https://map.heifahaizei.com/doc/overlay/point-collection.html -->
<bm-point-collection
:points="points"
shape="BMAP_POINT_SHAPE_STAR"
color="red"
size="BMAP_POINT_SIZE_SMALL"
>
</bm-point-collection>
<!-- 圆 https://map.heifahaizei.com/doc/overlay/circle.html -->
<bm-circle
:center="circlePath.center"
:radius="circlePath.radius"
stroke-color="blue"
:stroke-opacity="0.5"
:stroke-weight="2"
:editing="false"
:mass-clear="true"
></bm-circle>
控制弹框在地图范围内显示
el-dialog 只能在全局内显示,所以:手写 div 当弹框,让弹框跟地图点位同级,通过绝对定位实现
接收弹框组件的周边点位数据,并显示
注意:辐射圆单位是 m,画太小了,页面就显示不出辐射圆了(……)
/**
* 地图多个点集合
*/
const addPoints = (elems: any) => {
const pointAll = [];
for (const workSite of elems.workSiteResult) {
const position = { lng: workSite.longitude, lat: workSite.latitude };
pointAll.push(position);
}
state.points = pointAll;
// 填充辐射圆半径
state.circlePath.radius = elems.kilometerDistance * 1000;
// 填充辐射圆中心坐标
state.circlePath.center = elems.location;
};
【一个我很迷惑的报错】多个页面引用地图组件时,路由报错
我在查看、编辑页都引用了地图组件,就会出现下面的报错
后来发现,只要我把地图、地图组件内部的组件,再 cv 一份出来,分别引入到对应的页面,才能解决报错
如下所示,3 中的 edit、view 会引用地图组件,如果 3 中的 edit、view 同时引用了 1 或者同时引用了 2,那就会报错
3 中的 edit 引用 1,view 引用 2,就不会报错