1. 程式人生 > >vue+ElementUI+高德API地址模糊搜尋(自定義UI元件)

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;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(this.inputId === 'sname') { if(item.location && item.location.getLat()) { this.addForm.sname = item.name; this.addForm.slat = item.location.getLat(); this.addForm.slon = item.location.getLng(); this.pickAddress(this.inputId, this.addForm.slon, this.addForm.slat); 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>

 

效果圖: