这是一个h5页面–
效果图
录图的时候网络条件不是很好,wifi卡了,所以数据加载有点慢。当网络情况不好的时候,这个组件还需要进一步的优化
前置条件
- 在你的index.html页面上加载百度地图js(获取ak)
- 正确安装了vue
- 使用vue-cli可以快速开发
<script type="text/javascript" src="http://api.map.baidu.com/api?v=2.0&ak=yourAK"></script>
功能点分解
- 分析数据
- 实现
css
布局 - 挂载百度
map
- 定位当前位置
- 获取周边
poi
- 搜索提示
- 点击右下角图标重新定位到当前位置
- 固定中心点,拖拽地图选择位置
分析数据
根据UI图分析组件需要用到哪些数据。
data(){
return {
map:null,//实例化map
searchValue:"",//搜索model
currentAddress:"",//当前定位的地址
point:{},//当前地址的经纬度
poiKeyword:"",//周边的关键词,用来搜索,从adress中获取street拼接
potentialLocation:[],//周边信息
}
}
css布局
这样的布局很简单,请随意发挥
挂载百度map
initMaps(){
this.map = new BMap.Map("map");
let mPoint = new BMap.Point(116.404, 39.915);//天安门
this.map.centerAndZoom(mPoint,18);
}
为了确保dom
成功创建了,我们在nextTick
中进行初始化
mounted(){
this.$toast({text:"点击或拖动选址~",position:"top"});//提示信息
this.$nextTick(()=>{
this.initMaps();//调用初始化函数
})
到这里我们应该能在id
为map
中的div
里能够看到地图了
定位
我们调用百度提供的api进行定位,这里api定位的内部实现我猜测是基于h5的navigator。
根据警告提示,我们进去源码看看
警告信息说的是需要安全的origin
,这里由于是在本地开发环境我们可以忽略这个警告。到了线上还是需要https
的,不然遇到dns
劫持也是很麻烦的。
好的,现在实现locate方法用于定位:
locate(){
let map = this.map;
let geolocation = new BMap.Geolocation();
const vm = this;
geolocation.getCurrentPosition(function(r){
if(this.getStatus() === BMAP_STATUS_SUCCESS){
let mk = new BMap.Marker(r.point);
map.addOverlay(mk);
map.panTo(r.point);
}else {
console.log('failed ',this.getStatus());
}
});
这里我们就成功定位到了当前所在位置,当然别忘记了在mounted方法里调用这个方法了。
this.$nextTick(()=>{
this.initMaps();
this.locate();
})
显示当前位置
刚刚在locate方法中获取到了当前的point值,也就是经纬度。那么如何显示地址信息呢?locate中回调函数参数r其实是包括了address信息的,但是!有时候address精确度只是到了城市级别–比如我这里console.log(r);
打印出来的信息如下:
为了解决这个问题,我们需要从拿到的point
对象入手,逆地址解析一次。
这里我们创建一个analyze
函数调用百度的getLocation
方法进行解析
/**
* Attention: 解析地址会有异常--有时候会解析正确,有时候只会解析到区
* @param point lng and lat
*/
analyze(point){//point:{lat:"",lng:""}
const geoc = new BMap.Geocoder();
geoc.getLocation(point, rs=>{
this.point = rs.point;
this.currentAddress = rs.address;
this.poiKeyword = rs.street||rs.address;
});
},
并利用vue的数据响应相应的更新当前位置,当前经纬度,当前poiKeyword
。poiKeyword
有值了应该做什么呢?我们就可以开始根据point来搜索周边,提供周围的位置信息啦。
那么这里我们为poiKeyword
创建一个监听器,进行实时操作。
watch:{
poiKeyword(n){
this.getAroundPOI(["栋","店","小区","学校","餐饮",n]);//n就是最新值
}
}
实现getAroundPOI
函数
在监听器中我们提供了poi的搜索关键词,于是我们可以调用百度地图LocalSearch
的searchNearby
函数。这里我们搜索周围方圆1000m的周边数据。
getAroundPOI(keyword){
let map = this.map;
let mPoint = new BMap.Point(this.point.lng, this.point.lat);
let vm = this;
let local = new BMap.LocalSearch(map, {
onSearchComplete(results){
if (local.getStatus() === BMAP_STATUS_SUCCESS){
let temp = [];
results.forEach(item=>{
temp = temp.concat(item.Ar);
});
vm.potentialLocation = temp;//更新ui
}else{
console.warn("get poi error ,code -> ",local.getStatus());
}
}
});
local.searchNearby(keyword,mPoint,1000);
},
在onSearchComplete
回调中更新potentialLocation
的值之后vue
会帮助我们更新这部分ui
。
搜索提示
百度地图的搜索提示其实很简单,只需要创建一个自动装填对象。搜索下拉提示框会根据你传入的input
参数(input id)进行定位,宽度与input
相同。
getSuggestion(){
let ac = new BMap.Autocomplete({"input" : "suggestId","location" : this.map});
ac.addEventListener("onconfirm", e=> {
let _value = e.item.value;
this.searchValue = _value.province + _value.city + _value.district + _value.street + _value.business;
this.setPlace(this.searchValue);
});
},
在上述代码中,我们定义了自动装填对象的监听器(点击下拉列表项时触发),并更新了input
的值,在vue
中我们一般用v-model
进行双向绑定。
获取到了地址信息之后,我们调用了setPlace
方法并传入了搜索值。setPlace
方法主要用作给地图重定位,移动地图中心到搜索值所在地点。
setPlace(val){
let map = this.map;
map.clearOverlays();
const vm = this;
let local = new BMap.LocalSearch(map, {
onSearchComplete(){
let pp = local.getResults().getPoi(0).point;
map.centerAndZoom(pp, 18);
map.addOverlay(new BMap.Marker(pp));
vm.analyze(pp);
}
});
local.search(val);
}
同时,注意在上面的代码中我们调用了analyze
方法,用来更新周边信息。为什么能更新呢,忘了就往上翻回去看看analyze
方法是怎么实现的吧。
重定位及拖拽中心
为了实现这个需求,首先我们要想办法地图上的自定义控件。自定义控件很麻烦,而且也会影响网页性能,所以我们应该另辟蹊径。论坛上其实有更简单的方法,这里我将论坛中的思路实现了。
关键就在于将控件看成网页的dom
去相对于百度canvas
地图进行定位。
看一下dom
结构:
<div class="map-wrapper">
<div id="map"></div>
<img class="position" src="../../assets/icon/position.svg" alt="position">
<img class="nowposition" @click="locate" src="../../assets/icon/nowposition.svg" alt="nowposition">
</div>
以及css
.map-wrapper{
height: 50%;
position: relative;
#map{
height: 100%;
}
img{
width: 32px;
object-fit: contain;
}
.position{
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%,-75%);//The bottom of the icon is centered,75 = 50(center) + 25(top)
z-index: 100;
}
.nowposition{
position: absolute;
right: 20px;
bottom:20px;
z-index: 100;
}
}
这里要注意的一点就是利用position
以及translate(-50%,-50%)
居中以后为什么要上调25%?因为中间的图标是尖尖的,肉眼更倾向于利用图标的底部(那个尖尖)进行定位。我们给它的样式是正方形,并且是contain的,所以x轴是正好居中的不用管。但是y轴的问题是这样做仅仅是将y的中心点放在地图的中心位置,会有一点点的纬度偏差。我们应该与肉眼保持一致,因此需要将底部位于地图的正中心。因此我们还要上调25%。(自身高度的一半,因为就剩一半了)
为了能够让中心点定位,我们还需要给map注册一个监听器。
在initMaps
方法中加上一个监听器。
this.map.addEventListener('dragend',()=>{
let pixel = this.map.pointToOverlayPixel(this.map.getCenter());
let point = this.map.overlayPixelToPoint({x:pixel.x,y:pixel.y});
this.analyze(point);
})
在方法的回调里再调用analyze
方法重新获取poi
,实时更新周边位置信息。
源码
<template>
<div class="ys-map">
<div class="map-wrapper">
<div id="map"></div>
<img class="position" src="../../assets/icon/position.svg" alt="position">
<img class="nowposition" @click="locate" src="../../assets/icon/nowposition.svg" alt="nowposition">
</div>
<div id="tips">
<AddressItem :title="'当前位置'" :address="currentAddress" :extra="'(以图上标记位置为准)'"/>
<AddressItem v-for="(item,index) in potentialLocation" v-bind:key="index" :title="item.title" :address="item.address" @click.native="selectAddress(item)"/>
<div v-if="potentialLocation.length===0">{{point.lng}},{{point.lat}}</div>
</div>
<div class="ys-search-address">
<img class="back" src="../../assets/icon/back.svg" alt="back" @click="onBackClick">
<div class="ys-search-wrapper">
<input type="text" v-model="searchValue" title="" id="suggestId" placeholder="定位不准?试试手动输入">
<img src="../../assets/icon/close.svg" alt="search" @click="searchValue=''">
</div>
<a class="okBtn" href="javascript:;" @click="onOkClick">确定</a>
</div>
<div class="search-tips" id="result">
tips
</div>
</div>
</template>
<script>
import AddressItem from "./AddressItem";
export default {
name: "Map",
components: {AddressItem},
mounted(){
this.$toast({text:"点击或拖动选址~",position:"top"});
this.$nextTick(()=>{
this.initMaps();
this.locate();
this.getSuggestion();
})
},
methods:{
initMaps(){
this.map = new BMap.Map("map");
let mPoint = new BMap.Point(116.404, 39.915);//Tiananmen Square
this.map.centerAndZoom(mPoint,18);
this.map.addEventListener("click", this.onMapClicked);
this.map.addEventListener('dragend',()=>{
let pixel = this.map.pointToOverlayPixel(this.map.getCenter());
let point = this.map.overlayPixelToPoint({x:pixel.x,y:pixel.y});
this.analyze(point);
})
},
locate(){
let map = this.map;
let geolocation = new BMap.Geolocation();
const vm = this;
geolocation.getCurrentPosition(function(r){
if(this.getStatus() === BMAP_STATUS_SUCCESS){
let mk = new BMap.Marker(r.point);
map.addOverlay(mk);
map.panTo(r.point);
vm.analyze(r.point);
}else {
console.log('failed ',this.getStatus());
}
});
//loading--
},
getAroundPOI(keyword){
let map = this.map;
let mPoint = new BMap.Point(this.point.lng, this.point.lat);//h5 112.983323,28.141431
let vm = this;
let local = new BMap.LocalSearch(map, {
onSearchComplete(results){
if (local.getStatus() === BMAP_STATUS_SUCCESS){
let temp = [];
results.forEach(item=>{
temp = temp.concat(item.Ar);
});
vm.potentialLocation = temp;
}else{
console.warn("get poi error ,code -> ",local.getStatus());
}
}
});
local.searchNearby(keyword,mPoint,1000);
},
/**
* Attention: 解析地址会有异常--有时候会解析正确,有时候只会解析到区
* @param point lng and lat
*/
analyze(point){//point:{lat:"",lng:""}
const geoc = new BMap.Geocoder();
geoc.getLocation(point, rs=>{
this.point = rs.point;//===r.point
this.currentAddress = rs.address;
this.poiKeyword = rs.street||rs.address;
});
},
/**
* search tips
*/
getSuggestion(){
let ac = new BMap.Autocomplete({"input" : "suggestId","location" : this.map});
ac.addEventListener("onconfirm", e=> {
let _value = e.item.value;
this.searchValue = _value.province + _value.city + _value.district + _value.street + _value.business;
this.setPlace(this.searchValue);
});
},
selectAddress(item){
//todo: return item the same as onOkClicked function
},
setPlace(val){
let map = this.map;
map.clearOverlays();
const vm = this;
let local = new BMap.LocalSearch(map, {
onSearchComplete(){
let pp = local.getResults().getPoi(0).point;
map.centerAndZoom(pp, 18);
map.addOverlay(new BMap.Marker(pp));
vm.analyze(pp);
}
});
local.search(val);
},
onMapClicked(e){
console.log(e);//todo: get point with dialog and return
},
onBackClick(){
//todo: cancel
},
onOkClick(){
//todo: get this address and return.
console.log("已选中当前位置!");
console.log(this.currentAddress,this.point)
}
},
data(){
return {
map:null,
searchValue:"",
currentAddress:"",
point:{},
poiKeyword:"",
potentialLocation:[],
}
},
watch:{
poiKeyword(n){
this.getAroundPOI(["栋","店","小区","学校","餐饮",n]);
}
}
}
</script>
<style lang="less" scoped>
.ys-map{
height: 100%;
overflow: hidden;
position: relative;
.map-wrapper{
height: 50%;
position: relative;
#map{
height: 100%;
}
img{
width: 32px;
object-fit: contain;
}
.position{
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%,-75%);//The bottom of the icon is centered,75 = 50(center) + 25(top)
z-index: 100;
}
.nowposition{
position: absolute;
right: 20px;
bottom:20px;
z-index: 100;
}
}
#tips{
width: 100%;
height: 50%;
overflow-y: scroll;
position: relative;
padding: 1px 12px;
}
.ys-search-address{
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 50px;
background: #f2f2f2;
padding: 8px 0;
display: flex;
justify-content: space-around;
align-items: center;
.back{
width: 20px;
object-fit: contain;
}
.okBtn{
background: #FFDF5C;
padding: 5px 12px;
border: 1px solid #ffcf6e;
color: #333;
border-radius: 8px;
}
.ys-search-wrapper{
width: 68%;
border-radius: 20px;
background: rgba(0, 0, 0, .06);
display: flex;
align-items: center;
input{
padding: 8px 12px;
background: transparent;
width: 90%;
height: 100%;
}
img{
width: 12px;
object-fit: contain;
}
}
}
.search-tips{
position: absolute;
left: 0;
top: 50px;
background: rgba(255,255,255,.8);
border:1px solid #C0C0C0;
height:auto;
display:none;
}
}
</style>