开发环境描述:

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. flask + Python3 实现的的API自动化测试平台---- IAPTest接口测试平台,更名:FXTest 接受定制开发(java版开发完毕)

    **背景: 1.平时测试接口,总是现写代码,对测试用例的管理,以及测试报告的管理持久化做的不够,              2.工作中移动端开发和后端开发总是不能并行进行,需要一个mock的依赖来让他 ...

  2. docker_基础用法

    1. docker architecture 2. 命令

  3. 【大数据应用技术】作业十|分布式文件系统HDFS 练习

    本次作业的要求来自:https://edu.cnblogs.com/campus/gzcc/GZCC-16SE2/homework/3292 1.目录操作 在HDFS中为hadoop用户创建一个用户目 ...

  4. Centos 安装 zookeeper

    下载 下载地址:http://archive.apache.org/dist/zookeeper/ [root@localhost bin]# wget http://archive.apache.o ...

  5. 错误: -source 1.6 中不支持 diamond 运算符

    问题 错误: -source 1.6 中不支持 diamond 运算符 解决步骤 1.检查ide的默认编译环境 ,快捷键ctrl + alt +s 找Java Compiler ,发现设置是 Targ ...

  6. 012 spring retry重试原理的解析

    有点复杂,在后续的章节,将会对其中涉及到的知识点,再分章节进行说明. 1.程序结构 2.@Retryable package com.jun.web.annotation.theory; import ...

  7. Linux_CentOS 内存、cpu、进程、端口、硬盘管理

    内存.cup 管理 top 命令 top 1.top 命令的第一行: top - :: up :, users, load average: 0.00, 0.02, 0.05 依次对应:系统当前时间 ...

  8. 最稳定万能vip视频解析接口 支持HTTPS

    最稳定万能vip视频解析接口 支持HTTPS https://cdn.yangju.vip/k/?url=后面加上播放的地址即可 https://cdn.yangju.vip/k/?url= http ...

  9. k8s记录-不同集群服务互联

    1.外部访问内部服务 添加nodePort 修改type:ClusteIP为type:NodePort 2.内部服务访问外部服务 kubectl create -f test.yaml apiVers ...

  10. Linux开机报错,提示根目录有错误,无法通过检测进入系统

    报错信息如下: VolGroup-lv_root contains a file system with errors, check forced. 修复方法:(因为我的是虚拟机,可以随意做备份,所以 ...