开发环境描述:

Vue.js

ElementUI

高德地图API

需求描述:

在新增地址信息的时候,我们需要根据input输入的关键字调用地图的输入提示API,获取到返回的数据,并根据这些数据生成下拉列表,选择某一个即获取当前的地址相关信息(包括位置名称、经纬度、街区、城市、id等信息)。

如果不用鼠标选择,我们也可以按键盘上的上下方向键移动到目标地址,再按回车键选中目标地址。

实现方案分析:

1.使用Vue.js,为了复用性,我们考虑使用子组件来写。

2.当在input中输入关键字的时候,触发调用地图接口获取数据,也就是说要监听@input事件,在监听事件的回调函数中调用AMap.Autocomplete插件,搜索返回的数据传给子组件处理。

3.在子组件中要给每个地址绑定click事件,点击后把地址数据返回给父组件,还要给document绑定keydown事件(上、下方向键和回车键),另外,还要考虑地址下拉浮窗的显示位置(为了不受弹窗dialog的影响,将地址下拉浮窗附加到body元素下,并定位到input框的下方),以及当窗口大小变化(window.onresize)时,需要同时改变地址下拉浮窗的显示位置。

4.在父组件中也需要给document绑定click事件,当点击document其他位置时,隐藏子组件。

5.子组件在选择地址后,父组件把返回的数据进行处理:当经纬度存在时,直接赋值给相应的变量,当经纬度不存在时(当选择的是范围较大的地址时),调用地理编码API,可获取粗略的经纬度(比如广州市,调用地理编码API会返回广州市政府的经纬度),如果有需要,还可以显示地图,让用户可拖拽选址。

6.在组件销毁前(beforeDestroy),将document和window绑定的监听事件解绑。

具体实现:

之前写过一篇类似的随笔,使用的也是AMap.Autocomplete插件,不过使用的是高德地图定义好的UI和事件回调,页面中有几个地址输入框,就要定义多少个Autocomplete对象。具体请看这里

此篇我要写的是自定义的UI和事件回调。此方法复用性更强一点。

父组件:

<template>
<div style="margin: 50px;width: 300px;">
<el-form ref="addForm" v-model="addForm" :rules="addRules">
<el-form-item label="上车地点:" prop="sname">
<el-input id="sname" v-model.trim="addForm.sname" type="text"
@input="placeAutoInput('sname')" @keyup.delete.native="deletePlace('sname')"
placeholder="请输入上车地点">
<i
class="el-icon-location-outline el-input__icon"
slot="suffix" title="上车地点">
</i>
</el-input>
<div v-show="snameMapShow" class="map-wrapper">
<div>
<el-button type="text" size="mini" @click.stop="snameMapShow = false">收起<i class="el-icon-caret-top"></i></el-button>
</div>
<div id="sNameMap" class="map-self"></div></div>
</el-form-item>
</el-form>
<!--地址模糊搜索子组件-->
<place-search class="place-wrap"
ref="placeSearch"
v-if="resultVisible"
:result="result"
:left="offsetLeft"
:top="offsetTop"
:width="inputWidth"
:height="inputHeight"
@getLocation="getPlaceLocation"></place-search>
</div>
</template>
<script>
import AMap from 'AMap'
import placeSearch from './child/placeSearch' export default {
data() {
let validatePlace = (rules, value, callback) => {
if (rules.field === 'sname') {
if (value === '') {
callback(new Error('请输入上车地点'));
} else {
if (!this.addForm.slat || this.addForm.slat === 0) {
callback(new Error('请搜索并选择有经纬度的地点'));
} else {
callback();
}
}
}
};
return {
addForm: {
sname: '', // 上车地点
slat: 0, // 上车地点纬度
slon: 0 // 上车地点经度
},
addRules: {
sname: [{required: true, validator: validatePlace, trigger: 'change'}]
},
inputId: '', // 地址搜索input对应的id
result: [], // 地址搜索结果
resultVisible: false, // 地址搜索结果显示标识
inputWidth: 0, // 搜索框宽度
inputHeight: 0, // 搜索框高度
offsetLeft: 0, // 搜索框的左偏移值
offsetTop: 0, // 搜索框的上偏移值
snameMap: null, // 上车地点地图选址
snameMapShow: false, // 上车地点地图选址显示
}
},
components: {
'place-search': placeSearch
},
mounted() {
// document添加onclick监听,点击时隐藏地址下拉浮窗
document.addEventListener("click", this.hidePlaces, false);
// window添加onresize监听,当改变窗口大小时同时修改地址下拉浮窗的位置
window.addEventListener("resize", this.changePos, false)
},
methods: {
placeAutoInput(inputId) {
let currentDom = document.getElementById(inputId);// 获取input对象
let keywords = currentDom.value;
if(keywords.trim().length === 0) {
this.resultVisible = false;
}
AMap.plugin('AMap.Autocomplete', () => {
// 实例化Autocomplete
let autoOptions = {
city: '全国'
};
let autoComplete = new AMap.Autocomplete(autoOptions); // 初始化autocomplete
// 开始搜索
autoComplete.search(keywords, (status, result) => {
// 搜索成功时,result即是对应的匹配数据
if(result.info === 'OK') {
let sizeObj = currentDom.getBoundingClientRect(); // 取得元素距离窗口的绝对位置
this.inputWidth = currentDom.clientWidth;// input的宽度
this.inputHeight = currentDom.clientHeight + 2;// input的高度,2是上下border的宽
// input元素相对于页面的绝对位置 = 元素相对于窗口的绝对位置
this.offsetTop = sizeObj.top + this.inputHeight; // 距顶部
this.offsetLeft = sizeObj.left; // 距左侧
this.result = result.tips;
this.inputId = inputId;
this.resultVisible = true;
}
})
})
},
// 隐藏搜索地址下拉框
hidePlaces(event) {
let target = event.target;
// 排除点击地址搜索下拉框
if(target.classList.contains("address")) {
return;
}
this.resultVisible = false;
},
// 修改搜索地址下拉框的位置
changePos() {
if(this.inputId && this.$refs['placeSearch']) {
let currentDom = document.getElementById(this.inputId);
let sizeObj = currentDom.getBoundingClientRect(); // 取得元素距离窗口的绝对位置
// 元素相对于页面的绝对位置 = 元素相对于窗口的绝对位置
let inputWidth = currentDom.clientWidth;// input的宽度
let inputHeight = currentDom.clientHeight + 2;// input的高度,2是上下border的宽
let offsetTop = sizeObj.top + inputHeight; // 距顶部
let offsetLeft = sizeObj.left; // 距左侧
this.$refs['placeSearch'].changePost(offsetLeft, offsetTop, inputWidth, inputHeight);
}
},
// 获取子组件返回的位置信息
getPlaceLocation(item) {
if(item) {
this.resultVisible = false;
if(item.location && item.location.getLat()) {
this.pickAddress(this.inputId, item.location.getLng(), item.location.getLat());
this.$refs.addForm.validateField(this.inputId);
} else {
this.geocoder(item.name, this.inputId);
}
}
},
// 地图选址
pickAddress(inputId, lon, lat) {
if(inputId === "sname") {
this.snameMapShow = true;
AMapUI.loadUI(['misc/PositionPicker'], (PositionPicker) => {
this.snameMap = new AMap.Map('sNameMap', {
zoom: 16,
scrollWheel: false,
center: [lon,lat]
});
let positionPicker = new PositionPicker({
mode: 'dragMap',
map: this.snameMap
});
positionPicker.on('success', (positionResult) => {
this.addForm.slat = positionResult.position.lat;
this.addForm.slon = positionResult.position.lng;
this.addForm.sname = positionResult.address;
});
positionPicker.on('fail', (positionResult) => {
this.$message.error("地址选取失败");
});
positionPicker.start();
this.snameMap.addControl(new AMap.ToolBar({
liteStyle: true
}));
});
}
},
// 地理编码
geocoder(keyword, inputValue) {
let geocoder = new AMap.Geocoder({
//city: "010", //城市,默认:“全国”
radius: 1000 //范围,默认:500
});
//地理编码,返回地理编码结果
geocoder.getLocation(keyword, (status, result) => {
if (status === 'complete' && result.info === 'OK') {
let geocode = result.geocodes;
if (geocode && geocode.length > 0) {
if (inputValue === "sname") {
this.addForm.slat = geocode[0].location.getLat();
this.addForm.slon = geocode[0].location.getLng();
this.addForm.sname = keyword;
// 如果地理编码返回的粗略经纬度数据不需要在地图上显示,就不需要调用地图选址,且要隐藏地图
// this.pickAddress("sname", geocode[0].location.getLng(), geocode[0].location.getLat());
this.snameMapShow = false;
this.$refs.addForm.validateField("sname");
}
}
}
});
},
// 做删除操作时还原经纬度并验证字段
deletePlace(inputId) {
if (inputId === "sname") {
this.addForm.slat = 0;
this.addForm.slon = 0;
this.$refs.addForm.validateField("sname");
}
}
},
beforeDestroy() {
document.removeEventListener("click", this.hidePlaces, false);
}
}
</script>
<style>
.map-wrapper .map-self{
height: 150px;
}
</style>

备注:在data()中定义的inputId是为了保存当前操作的输入框id,在子组件返回选择的数据时可根据inputId给该input对应的相关变量赋值,另外,所有的if (inputId === "sname")语句都是为了防止混淆不同input对应的变量(字段),如不需要可删除此语句。

子组件:placeSearch.vue

这里给每个元素都加上了一个class:“address”,作用是在document的点击事件中,如果事件对象含有该class,不隐藏地址下拉浮窗。

另外要注意,API返回的数据虽然都有id、address属性(不为空时都是字符串格式),但会出现返回的id、address为空值(空字符串),故给li设置的key尽量不要用API返回的id(空值时设置给:key会报错),而是用自定义的索引值index,当address为空时,类型是Array(且长度为0),会显示[],为了防止这种情况,我们显示district属性的值就可以了。

<template>
<div class="result-list-wrapper" ref="resultWrapper">
<ul class="result-list address" :data="result">
<li class="result-item address"
v-for="(item, index) in result"
:key="item.index"
@click="setLocation(item)"
ref="resultItem">
<p class="result-name address" :class="{'active': index === activeIndex}">{{item.name}}</p>
<template v-if="item.address instanceof Array"><p class="result-adress address">{{item.district}}</p></template>
<template v-else><p class="result-adress address">{{item.address}}</p></template>
</li>
</ul>
</div>
</template>
<script type="text/ecmascript-6">
export default {
props: {
result: {
type: Array,
default: null
},
left: { // 输入框的offsetLeft
type: Number,
default: 0
},
top: { // 输入框的offsetTop
type: Number,
default: 0
},
width: { // 输入框的宽
type: Number,
default: 0
},
height: { // 输入框的高
type: Number,
default: 0
}
},
data() {
return {
activeIndex: 0 // 激活项
}
},
methods: {
// 选择下拉的地址
setLocation(item) {
this.$emit('getLocation', item)
},
// 初始化地址搜索下拉框位置
initPos() {
let dom = this.$refs['resultWrapper'];
let body = document.getElementsByTagName("body");
if(body) {
body[0].appendChild(dom);
let clientHeight = document.documentElement.clientHeight;
let wrapHeight = 0;
if(this.result && this.result.length>5) {
wrapHeight = 250;
} else if(this.result && this.result.length<=5) {
wrapHeight = this.result.length * 50;
}
if(clientHeight - this.top < wrapHeight) {
// 如果div高度超出底部,div往上移(减去div高度+input高度)
dom.style.top = this.top - wrapHeight - this.height + 'px';
} else {
dom.style.top = this.top + 'px';
}
dom.style.left = this.left + 'px';
dom.style.width = this.width + 'px'
}
},
// 窗口resize时改变下拉框的位置
changePost(left, top, width, height) {
let dom = this.$refs['resultWrapper'];
let clientHeight = document.documentElement.clientHeight;
let wrapHeight = 0;
if(this.result && this.result.length>5) {
wrapHeight = 250;
} else if(this.result && this.result.length<=5) {
wrapHeight = this.result.length * 50;
}
if(clientHeight - top < wrapHeight) {
// 如果div高度超出底部,div往上移(减去div高度+input高度)
dom.style.top = top - wrapHeight - height + 'px';
} else {
dom.style.top = top + 'px';
}
dom.style.left = left + 'px';
dom.style.width = width + 'px'
},
// 监听键盘上下方向键并激活当前选项
keydownSelect(event) {
let e = event || window.event || arguments.callee.caller.arguments[0];
if(e && e.keyCode === 38){//上
if(this.$refs['resultWrapper']) {
let items = this.$refs['resultWrapper'].querySelectorAll(".result-item");
if(items && items.length>0) {
this.activeIndex--;
// 滚动条往上滚动
if(this.activeIndex < 5) {
this.$refs['resultWrapper'].scrollTop = 0
}
if(this.activeIndex === 5) {
this.$refs['resultWrapper'].scrollTop = 250
}
if(this.activeIndex === -1) {
this.activeIndex = 0;
}
}
}
} else if(e && e.keyCode === 40) {//下
if(this.$refs['resultWrapper']) {
let items = this.$refs['resultWrapper'].querySelectorAll(".result-item");
if(items && items.length>0) {
this.activeIndex++;
// 滚动条往下滚动
if(this.activeIndex === 5) {
this.$refs['resultWrapper'].scrollTop = 250
}
if(this.activeIndex === 9) { // 防止最后一条数据显示不全
this.$refs['resultWrapper'].scrollTop = 300
}
if(this.activeIndex === items.length) {
this.activeIndex = 0;
this.$refs['resultWrapper'].scrollTop = 0
}
}
}
} else if(e && e.keyCode === 13) { // 监听回车事件,并获取当前选中的地址的经纬度等信息
if(this.result && this.result.length > this.activeIndex) {
this.setLocation(this.result[this.activeIndex]);
}
}
}
},
mounted() {
this.initPos();
document.addEventListener("keydown", this.keydownSelect, false);
},
beforeDestroy() {
document.removeEventListener("keydown", this.keydownSelect, false);
}
}
</script>
<style lang="stylus" scoped>
.result-list-wrapper
position absolute
max-height 250px
overflow auto
z-index: 9999
border: 1px solid #ccc
background-color: #fff
.result-list
.result-item
padding 5px
color #666
border-bottom 1px solid #ccc
&:hover
background-color: #f5f5f5
cursor pointer
&:last-child
border-bottom none
.result-name
font-size 12px
margin-bottom 0.5rem
&.active
color #259bff
.result-adress
font-size 12px
color #bbb
</style>

效果图:

vue+ElementUI+高德API地址模糊搜索(自定义UI组件)的更多相关文章

  1. 基于Vue的Quasar Framework 介绍 这个框架UI组件很全面

    基于Vue的Quasar Framework 介绍 这个框架UI组件很全面 基于Vue的Quasar Framework 中文网http://www.quasarchs.com/ quasarfram ...

  2. iOS(Swift)学习笔记之SnapKit+自定义UI组件

    本文为原创文章,转载请标明出处 1. 通过CocoaPods安装SnapKit platform :ios, '10.0' target '<Your Target Name>' do u ...

  3. Vue+ElementUI: 手把手教你做一个audio组件

    目的 本项目的目的是教你如何实现一个简单的音乐播放器(这并不难) 本项目并不是一个可以用于生产环境的element播放器,所以并没有考虑太多的兼容性问题 本项目不是ElementUI的一个音频插件,只 ...

  4. vux 是基于 WeUI 和Vue(2.x)开发的移动端UI组件库,主要服务于微信页面。

    https://doc.vux.li/zh-CN/ https://vux.li/

  5. VUE常用UI组件插件及框架

    UI组件及框架 element - 饿了么出品的Vue2的web UI工具套件 mint-ui - Vue 2的移动UI元素 iview - 基于 Vuejs 的开源 UI 组件库 Keen-UI - ...

  6. 【分享】Vue 资源典藏(UI组件、开发框架、服务端、辅助工具、应用实例、Demo示例)

    Vue 资源典藏,包括:UI组件 开发框架 服务端 辅助工具 应用实例 Demo示例 element ★11612 - 饿了么出品的Vue2的web UI工具套件 Vux ★7503 - 基于Vue和 ...

  7. 16款优秀的Vue UI组件库推荐

    16款优秀的Vue UI组件库推荐 Vue 是一个轻巧.高性能.可组件化的MVVM库,API简洁明了,上手快.从Vue推出以来,得到众多Web开发者的认可.在公司的Web前端项目开发中,多个项目采用基 ...

  8. vue.js相关UI组件收集

    内容 UI组件 开发框架 实用库 服务端 辅助工具 应用实例 Demo示例 ###UI组件 element ★9689 - 饿了么出品的Vue2的web UI工具套件 Vux ★6927 - 基于Vu ...

  9. 强烈推荐优秀的Vue UI组件库

    Vue 是一个轻巧.高性能.可组件化的MVVM库,API简洁明了,上手快.从Vue推出以来,得到众多Web开发者的认可.在公司的Web前端项目开发中,多个项目采用基于Vue的UI组件框架开发,并投入正 ...

随机推荐

  1. C# System.Web.Mail.MailMessage 发邮件

    C# System.Web.Mail.MailMessage 发邮件 新建控制台Console项目,然后添加 System.Web引用 代码如下: using System; using System ...

  2. Java基础 三目运算符 用if-else对其进行解释

        JDK :OpenJDK-11      OS :CentOS 7.6.1810      IDE :Eclipse 2019‑03 typesetting :Markdown   code ...

  3. Jmeter多业务混合场景如何设置各业务所占并发比例

    在进行多业务混合场景测试中,需要分配每个场景占比. 具体有两种方式: 1.多线程组方式: 2.逻辑控制器控制: 第一种: jmeter一个测试计划可以添加多个线程组,我们把不同的业务放在不同的线程组中 ...

  4. (转)scipy详解

    原文:https://www.cnblogs.com/ws0751/p/8361353.html#top 登月图片消噪   scipy.fftpack模块用来计算快速傅里叶变换速度比传统傅里叶变换更快 ...

  5. CefSharp 提示 flash player is out of date 运行此插件 等问题解决办法

    CefSharp 提示 flash player is out of date 或者 需要手动右键点 运行此插件 脚本 等问题解决办法 因为中国版FlashPlayer变得Ad模式之后,只好用旧版本的 ...

  6. centos 安装 libiconv

    安装方法如下: cd /usr/local/src wget http://ftp.gnu.org/pub/gnu/libiconv/libiconv-1.14.tar.gz tar -zxvf li ...

  7. linux广播

    linux广播 // 发送端 #include <stdio.h> #include <unistd.h> #include <sys/types.h> #incl ...

  8. 【linux学习笔记七】关机重启命令

    shutdown命令 shutdown [选项] 时间 #-c 取消前一个关机命令 #-h 关机(慎用远程关机) #-r 重启 其它关机命令 halt poweroff init 0  其它重启命令 ...

  9. IDEA 2018 搭建 Spring MVC helloworld

    转自https://segmentfault.com/a/1190000017248622 网上看了不少idea搭建SpringMVC Helloworld的例子,但是一个个试下来都没有成功.我把他们 ...

  10. SOC中的DMIPS_GFLOPS_GMACS的含义

    l  DMIPS全称叫Dhrystone MIPS 这项测试是用来计算同一秒内系统的处理能力,它的单位以百万来计算,也就是(MIPS) 上面的意思也就是,这个处理器测整数计算能力为(200*100万) ...