zoukankan      html  css  js  c++  java
  • vue+ElementUI+高德API地址模糊搜索(自定义UI组件)

    开发环境描述:

    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; 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
                },
                 { // 输入框的宽
                    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>

    效果图:

  • 相关阅读:
    【LabVIEW】多列列表框使用汇总
    【LabVIEW】数据类型 汇总
    U-BOOT移植 前准备
    linux 的 输入子系统 与 平台设备系统个人理解
    关于内核编译的理解
    关于 内核编译make menuconfig 不能使用的解决
    函数式接口的使用 (Function、Predicate、Supplier、Consumer)
    获取单列集合,双列集合,数组的Stream流对象以及简单操作
    多线程的创建、匿名内部类方式创建线程、定义、调度步骤
    异常类的使用
  • 原文地址:https://www.cnblogs.com/yeqrblog/p/10083895.html
Copyright © 2011-2022 走看看