可自定義的下拉框和選中樣式的遠程搜索下拉選擇框

代碼佔位置比較多,因此實現方式和遇到問題請跳轉到另外一篇文章,此文只爲展現代碼

實現方式和遇到問題html

組件支持的配置

* deleteChecked方法在刪除輸入框中的一個tag標籤時觸發   參數爲被刪除的對象
        * clearChecked方法在清空輸入框中的全部tag標籤時觸發   參數爲刪除前的數組單選時爲對象
        * valueChange方法在輸入框內容發生改變時觸發  參數爲輸入框修改後的值 在輸入框內容變爲空時並不會觸發,須要觸發則加上 emptyReturn 或 emptyReturn="true"
        * clickTag方法在點擊輸入框中的tag時觸發  參數爲點擊的tag的對象
        * v-model雙向綁定的爲選中的數組(多選時爲數組,單選時爲對象)
        * options爲下拉框渲染數據用的數組單選時爲對象
        * multiple爲輸入框是否爲多選 只在標籤上寫multiple 或寫 multiple="true"則爲多選,其餘狀況均爲單選  默認爲單選 
        * width爲輸入框寬度  默認爲100%
        * tag插槽爲輸入框中已選中對象的展示樣式,slot-scope=「{ item }」 接收的爲選中的對象,默認爲只顯示item中label
        * option插槽爲下拉選擇框的展示樣式,slot-scope=「{ item }」 接收的爲選中的對象,默認爲只顯示item中label
        * props爲配置選項包含如下內容:
                參數	            說明	               類型        默認值
                value	指定選項的值爲選項對象的某個屬性值	string		'value'
                label	指定選項標籤爲選項對象的某個屬性值	string		'label'
        * 支持事件: input  focus  blur  change  keyup keydown 等全部適用於input的事件
        * isEN爲是否爲英文環境 true 爲英文 不傳或false 爲中文
        * antiShake爲輸入框內容改變的防抖時間 antiShake="200"  單位爲ms  不傳則默認爲0
複製代碼

組件調用示例

<template>
    <div>
        <!--
            * deleteChecked方法在刪除輸入框中的一個tag標籤時觸發   參數爲被刪除的對象
            * clearChecked方法在清空輸入框中的全部tag標籤時觸發   參數爲刪除前的數組單選時爲對象
            * valueChange方法在輸入框內容發生改變時觸發  參數爲輸入框修改後的值 在輸入框內容變爲空時並不會觸發,須要觸發則加上 emptyReturn 或 emptyReturn="true"
            * clickTag方法在點擊輸入框中的tag時觸發  參數爲點擊的tag的對象
            * v-model雙向綁定的爲選中的數組
            * options爲下拉框渲染數據用的數組單選時爲對象
            * multiple爲輸入框是否爲多選 只在標籤上寫multiple 或寫 multiple="true"則爲多選,其餘狀況均爲單選  默認爲單選
            * draggable爲輸入框是否爲多選 只在標籤上寫draggable 或寫 draggable="true"則爲多選,其餘狀況均爲單選  默認爲單選
            * width爲輸入框寬度  默認爲100%
            * tag插槽爲輸入框中已選中對象的展示樣式,slot-scope=「{ item }」 接收的爲選中的對象,默認爲只顯示item中label
            * option插槽爲下拉選擇框的展示樣式,slot-scope=「{ item }」 接收的爲選中的對象,默認爲只顯示item中label
            * props爲配置選項包含如下內容:
                    參數	            說明	               類型        默認值
                    value	指定選項的值爲選項對象的某個屬性值	string		'value'
                    label	指定選項標籤爲選項對象的某個屬性值	string		'label'
            * 支持事件: input  focus  blur  change  keyup keydown 等全部適用於input的事件
            * isEN爲是否爲英文環境 true 爲英文 不傳或false 爲中文
            * antiShake爲輸入框內容改變的防抖時間 antiShake="200"  單位爲ms  不傳則默認爲0
            * 調用子組件的changeOptionsShow方法能夠顯示搜索結果
        -->
        <Myinput v-model="arr" @valueChange="valueChange" :options="options" multiple draggable :isEN="false" width="400px" :props="props" ref="myInput">
            <template slot="tag" slot-scope="{ item }">
                <img src="./img.jpg" style="width: 16px; height: 16px; border-radius: 50%;">
                <div style="line-height: 14px; font-size: 14px; margin-left:4px; word-wrap: break-word; word-break: break-word;">
                    {{item.name}}
                </div>
            </template>
            <template slot="option" slot-scope="{ item }">
                <div class="optionStyle">
                    <img class="headPortrait" src="./img.jpg">
                    <div class="right">
                        <div class="top">
                            <div class="name" v-html="item.newname"></div>
                            <div class="email">{{item.email}}</div>
                        </div>
                        <div class="bottom">{{item.department}}</div>
                    </div>
                </div>
            </template>
        </Myinput>


    </div>
</template>

<script lang='ts'>
import Myinput from './selectInput.vue'
    import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
    @Component({
        components: {
            Myinput
        }
    })
    export default class extends Vue {
        private list: any[] = [
            {name: '01胖弟弟', email: '1111@qq.com', department: 'oneone', id: 1},
            {name: '02五五開', email: '2222@qq.com', department: 'twotwo', id: 2},
            {name: '03孫亞龍', email: '3333@qq.com', department: 'threethree', id: 3},
            {name: '04盧本偉', email: '4444@qq.com', department: 'fourfour', id: 4},
            {name: '05卡卡西', email: '5555@qq.com', department: 'fivefive', id: 5},
            {name: '06大蛇丸', email: '6666@qq.com', department: 'fivefive', id: 6},
            {name: '07自來也', email: '7777@qq.com', department: 'fivefive', id: 7},
            {name: '08鳴人', email: '7777@qq.com', department: 'fivefive', id: 8},
            {name: '09佐助', email: '7777@qq.com', department: 'fivefive', id: 9},
            {name: '10小櫻', email: '7777@qq.com', department: 'fivefive', id: 10},
            {name: '11寧次', email: '7777@qq.com', department: 'fivefive', id: 11},
            {name: '12小李', email: '7777@qq.com', department: 'fivefive', id: 12}
        ]
        private props: object = {
            value: 'id',
            label: 'name'
        }
        private arr: any = []
        private options: any[] = []
        private valueChange(val: string) {
            if (val === '') {
                this.options = []
                return
            }
            this.options = this.list.filter(item => {
                return item.name.includes(val)
            })
            this.options = this.options.map(item => {
                item.newname = item.name
                item.newname = item.newname.replace(new RegExp(val, 'g'), `<span style="color: #3C8CFF;">${val}</span>`)
                return item
            })
            let temp: any = this.$refs.myInput
            temp.changeOptionsShow()
        }
    }
</script>

<style scoped>
.optionStyle {
    display: flex;
    align-items: center;
    padding: 0 12px;
    min-height: 48px;
    cursor: pointer;
}
.optionStyle:hover {
    background-color: #f5f7fa;
}
.optionStyle .headPortrait {
    width: 32px;
    height: 32px;
    border-radius: 50%;
}
.optionStyle .right {
    padding: 7px 0 7px 12px;
}
.optionStyle .top {
    display: flex;
    flex-wrap: wrap;
    min-height: 18px;
    margin-bottom: 2px;
}
.optionStyle .bottom {
    min-height: 14px;
    line-height: 14px;
    font-size: 12px;
}
.optionStyle .top .name{
    font-size: 14px;
    line-height: 14px;
    margin-right: 8px;
}
.optionStyle .top .email{
    line-height: 12px;
    font-size: 12px;
}
</style>
複製代碼

組件代碼

注:代碼中使用的圖標爲阿里圖標庫svg的×圖標,可自行下載或替換爲其餘圖標(連接vue

<template>
    <div :style="inputWidth" class="demo-XL">
        <div class="demo-outBox-XL" @click="chooseInput" :class="{'demo-is-focus-XL': inputFocus}">
            <span @click.stop="clearChecked">
                <img src="./assets/crossIcon.svg" class="demo-InputCloseIcon-XL" >
            </span>
            <!-- 多選 -->
            <div class="demo-chooseContent-XL" :class="{'demo-showInput-XL': !showClearIcon}" v-if="isMultiple">
                <div v-for="(item, i) in checkedArr" :key="'checked' + i" class="demo-checkedTag-XL"
                    :draggable="isDraggable && !deleteStatus"
                    @dragstart="handleDragStart($event, item)"
                    @dragover.prevent="handleDragOver($event, item)"
                    @dragenter="handleDragEnter($event, item)"
                    @dragend="handleDragEnd($event, item)"
                >
                    <div class="demo-outContent-XL" @click="chooseTag(item)" :class="{'demo-deleteStatus-XL': i === checkedArr.length - 1 && deleteStatus}">
                        <slot name="tag" v-bind:item="item">{{item[props.label]}}</slot>
                        <span @click.stop="deleteChecked(item)" class="demo-icon-XL">
                            <img src="./assets/crossIcon.svg" class="demo-tagIcon-XL" >
                        </span>
                    </div>
                </div>
                <input type="text" v-model.trim="searchVal" :placeholder="!checkedArr.length ? placeholder : ''" class="demo-inInput-XL" @focus="handleFocus" @blur="loseFocus" ref="inInput" @keydown.8="deleteOne" v-on="$listeners">
            </div>
            <!-- 單選 -->
            <div class="demo-chooseContent-XL" :class="{'demo-showInput-XL': !showClearIcon}"  v-else>
                <div class="demo-checkedTag-XL" v-if="checkedArr[props.label]">
                    <div class="demo-outContent-XL" @click="chooseTag(checkedArr)" :class="{'transparent-XL': hasFocus}" style="position: absolute;">
                        <slot name="tag" v-bind:item="checkedArr">{{checkedArr[props.label]}}</slot>
                        <span @click.stop="deleteChecked(checkedArr)" class="demo-icon-XL">
                            <img src="./assets/crossIcon.svg" class="demo-tagIcon-XL" >
                        </span>
                    </div>
                </div>
                <input type="text" v-model.trim="searchVal" :placeholder="!checkedArr[props.value] ? placeholder : ''" class="demo-inInput-XL" @focus="handleFocus" @blur="loseFocus" ref="inInput" v-on="$listeners" style="z-index: 10; background: transparent;">
            </div>
        </div>
        <div class="demo-chooseOption-XL" v-if="showOptions" :style="inputWidth">
            <div v-for="(item, i) in optionsList" :key="'option' + i" @click="chooseOption(item)">
                <slot name="option" v-bind:item="item">
                    <div class="demo-option-XL" >{{item[props.label]}}</div>
                </slot>
            </div>
            <div v-if="!optionsList.length" class="nonePeople-XL">
                {{isEN ? 'No result for search' : '沒有搜索到結果'}}
            </div>
        </div>
    </div>
</template>

<script lang='ts'>
    import { Component, Prop, Vue, Watch, Emit, Model } from 'vue-property-decorator';
    @Component
    export default class  extends Vue {
        // 語言環境
        @Prop({type: Boolean, default: false}) private isEN!: boolean
        // 防抖延時時間
        @Prop({default: 0}) private antiShake!: any
        // 下拉框的數據
        @Prop({type: Array, default: []}) private options!: any[]
        // 輸入框爲空時是否調用父組件內容改變方法
        @Prop({type: String, default: 'false'}) private emptyReturn!: string
        // 是否多選
        @Prop({type: String, default: 'false'}) private multiple!: string
        // 是否支持拖拽
        @Prop({type: String, default: 'false'}) private draggable!: string
        // 輸入框組件寬度
        @Prop({type: String, default: ''}) private width!: string
        // 下拉框和tag顯示配置項
        @Prop({type: Object, default: {value: 'value', label: 'label'}}) private props!: any
        // 已選擇的對象(v-model的雙向綁定)
        @Model ('changeValue')  value !:  any[] | object
        private searchVal: string = ''
        private inputFocus: boolean = false
        private showClearIcon: boolean = true
        private inputWidth: object = {}
        private hasFocus: boolean = false
        private optionsList: any[] = []
        private showOptions: boolean = false
        // 暫時存儲延時器 防抖
        private timeoutTemp: any = null
        // 刪除前的刪除狀態 是否出現
        private deleteStatus: boolean = false
        get isMultiple() {
            return this.multiple === '' || this.multiple === 'true'
        }
        get isDraggable() {
            return this.draggable === '' || this.draggable === 'true'
        }
        // 輸入框佔位符
        get placeholder() {
            if (this.isEN) {
                return 'Search using name or email'
            } else {
                return '搜索輸入姓名或郵箱'
            }
        }
        private mounted() {
            if (this.width) {
                this.inputWidth = {
                    width: this.width
                }
            }
        }
        // 選中的數組
        get checkedArr() {
            return this.value
        }
        set checkedArr(val: any) {
            this.changeValue(val)
        }
        private deleteOne() {
            if (!this.searchVal && this.checkedArr.length) {
                if (this.deleteStatus) {
                    this.checkedArr.pop()
                    this.deleteStatus = false
                } else {
                    this.deleteStatus = true
                }
            }
        }
        private changeOptionsShow() {
            this.showOptions = true
        }
        // 輸入框得到焦點
        private handleFocus() {
            this.inputFocus = true
            this.showClearIcon = false
            this.hasFocus = true
        }
        // 輸入框失去焦點
        private loseFocus() {
            setTimeout(() => {
                // this.inputFocus = false
                if (this.focusFlag) {
                    this.focusFlag = false
                } else {
                    this.inputFocus = false
                    this.searchVal = ''
                }
                if (this.isMultiple) {
                    this.showClearIcon = !this.checkedArr.length
                } else {
                    this.showClearIcon = !this.checkedArr[this.props.value]
                }
                this.hasFocus = false
                this.showOptions = false
                this.deleteStatus = false
            }, 500);
        }
        // 選擇下拉框內容
        private focusFlag = false
        private chooseOption(val: any) {
            this.showOptions = false
            if (this.isMultiple) {
                let tempDom: any = this.$refs.inInput
                setTimeout(() => {
                    tempDom.focus()
                }, 200);
                this.focusFlag = true
                let temp: any = this.checkedArr.find((item: any) => item[this.props.value] === val[this.props.value] )
                if (temp) {
                    this.checkedArr = this.checkedArr.filter((item: any) => {
                        return item[this.props.value] !== val[this.props.value]
                    })
                } else {
                    this.checkedArr.push(val)
                }
                this.searchVal = ''
            } else {
                this.checkedArr = val
                this.searchVal = ''
                this.hasFocus = false
            }
        }
        // 選擇輸入框進行輸入
        private chooseInput() {
            let temp: any = this.$refs.inInput
            temp.focus()
        }
        // 清空輸入框中的tag
        private clearChecked() {
            let temp = this.checkedArr
            setTimeout(() => {
                this.checkedClear(temp)
            }, 0);
            if (this.isMultiple) {
                this.checkedArr = []
            } else {
                this.checkedArr = {}
            }
            this.valueChange(this.searchVal)
        }
        // 刪除輸入框中的tag的操做
        private deleteChecked(val: any) {
            if (this.isMultiple) {
                let temp: any = this.checkedArr.filter((item: any) => {
                    return item !== val
                })
                this.checkedArr = temp
                this.checkedDelete(val)
            } else {
                this.checkedArr = {}
                this.checkedDelete(val)
            }
        }
        // 點擊輸入框中的tag的操做
        private chooseTag(val: any) {
            this.clickTag(val)
        }
        // 用來修改雙向綁定的選中數組的值
        @Emit('changeValue')
        changeValue(val: any) {
        }
        // 調用父組件中valueChange方法 來修改options的值
        @Emit('valueChange')
        valueChange(val: any) {}
        // 刪除已選中的數組調用的方法
        @Emit('deleteChecked')
        checkedDelete(val: any) {}
        // 清空已選中的數組調用的方法
        @Emit('clearChecked')
        checkedClear(val: any) {}
        // 點擊輸入框的tag標籤調用的方法
        @Emit('clickTag')
        clickTag(val: any) {}
        // 已選中數組改變時判斷是否顯示placeholder
        @Watch('checkedArr')
        checkedArrChange(newArr: any) {
            if (this.isMultiple) {
                this.showClearIcon = !this.checkedArr.length && !this.inputFocus
            } else {
                this.showClearIcon = !newArr[this.props.value] && !this.inputFocus
            }
        }
        @Watch('searchVal')
        searchValChange(newVal: any) {
            this.deleteStatus = false
            if (this.timeoutTemp) {
                clearTimeout(this.timeoutTemp)
            }
            this.timeoutTemp =  setTimeout(() => {
                this.showOptions = false
                if (newVal || this.emptyReturn === '' || this.emptyReturn === 'true') {
                    this.valueChange(this.searchVal)
                } else {
                    this.optionsList = []
                }
                this.timeoutTemp = null
            }, this.antiShake - 0);
        }
        @Watch('options')
        optionsChange(newVal: any) {
            this.optionsList = newVal
        }
        // 實現拖拽功能
        private dragging: any = null;
        private handleDragStart(e: any, item: object): void {
            this.dragging = item;
        }
        private handleDragEnd(e: any, item: object): void {
            this.dragging = null;
        }

        private handleDragOver(e: any, item: object): void {
            e.dataTransfer.dropEffect = "move"; // e.dataTransfer.dropEffect="move";//在dragenter中針對放置目標來設置!
        }

        private handleDragEnter(e: any, item: object): void {
            e.dataTransfer.effectAllowed = "move"; //爲須要移動的元素設置dragstart事件
            if (item === this.dragging) {
            return;
            }
            const newArr = [...this.checkedArr];
            const src = newArr.indexOf(this.dragging);
            const dst = newArr.indexOf(item);
            newArr.splice(dst, 0, ...newArr.splice(src, 1));
            this.checkedArr = newArr;
        }
        
    }
</script>

<style scoped>
.demo-deleteStatus-XL {
    opacity: 0.3;
}
    .demo-XL {
        background: #fff;
        margin-bottom: 16px;
        position: relative;

    }
    .demo-XL .demo-outBox-XL {
        max-height: 88px;
        /* position: relative; */
        min-height: 28px;
        outline: none;
        border: 1px solid rgba(217,217,217,1);
        border-radius: 2px;
        padding: 4px 0 0;
        cursor: text;
        overflow: auto;
        overflow-x: hidden;
    }
    /*定義滾動條高寬及背景 高寬分別對應橫豎滾動條的尺寸*/
    .demo-XL .demo-outBox-XL::-webkit-scrollbar
    {
        width: 4PX;
        height: 0;
    }
    /*定義滑塊 內陰影+圓角*/
    .demo-XL .demo-outBox-XL::-webkit-scrollbar-thumb
    {
        background:rgba(206,206,206,1);
        border-radius:2px;
        width: 4px;
    }
    .demo-XL .demo-outBox-XL:hover {
        border: 1px solid #3C8CFF;
    }
    .demo-XL .demo-outBox-XL:hover .demo-InputCloseIcon-XL {
        display: block;
    }
    .demo-XL .demo-is-focus-XL .demo-InputCloseIcon-XL {
        display: block;
    }
    .demo-XL .demo-placeholder-XL {
        position: absolute;
        z-index: 10;
        left: 12px;
        top: 5px;
        font-size: 14px;
        height: 20px;
        line-height: 20px;
        color: #ccc;
    }
    .demo-XL .demo-is-focus-XL.demo-outBox-XL {
        border: 1px solid #3C8CFF;
    }
    .demo-XL .demo-inInput-XL {
        height: 24px;
        min-width: 20px;
        outline: none;
        border: 0 none;
        padding: 0;
        flex: 1;
    }
    .demo-XL .demo-chooseContent-XL {
        margin-left: 12px;
        width: calc(100% - 46px);
        display: flex;
        flex-wrap: wrap;
    }
    .demo-XL .demo-chooseContent-XL.demo-showInput-XL {
        z-index: 11;
    }
    .demo-XL .demo-InputCloseIcon-XL {
        width: 20px;
        height: 20px;
        position: absolute;
        right: 7px;
        top: 7px;
        z-index: 12;
        cursor: pointer;
        display: none;
    }
    .demo-XL .demo-checkedTag-XL {
        box-sizing: border-box;
        min-height: 24px;
        border-radius: 2px;
        margin-right: 4px;
        margin-bottom: 4px;
        background: #f5f5f5;
        font-size: 14px;
    }
    .demo-XL .demo-outContent-XL {
        display: flex;
        align-items: center;
        padding: 4px 8px;
        cursor: pointer;
        min-height: 14px;
    }
    .demo-XL .demo-outContent-XL .demo-icon-XL {
        height: 14px;
        cursor: pointer;
        margin-left: 4px;
    }
    .demo-XL .demo-outContent-XL span .demo-tagIcon-XL {
        width: 14px;
        height: 14px;
        vertical-align: top;
    }
    .demo-XL .demo-option-XL {
        height: 34px;
        line-height: 34px;
        font-size: 14px;
        cursor: pointer;
        padding: 0 20px;
    }
    .demo-XL .demo-option-XL:hover {
        background-color: #f5f5fa;
    }
    .demo-XL .demo-chooseOption-XL {
        box-sizing: border-box;
        position: absolute;
        max-height: 208px;
        width: 100%;
        padding: 8px 0;
        margin-top: 4px;
        box-shadow:0px 0px 4px 0px rgba(0,0,0,0.1);
        border-radius:2px;
        overflow: auto;
        overflow-x: hidden;
        background: #fff;
        z-index: 10;
    }
    /*定義滾動條高寬及背景 高寬分別對應橫豎滾動條的尺寸*/
    .demo-XL .demo-chooseOption-XL::-webkit-scrollbar
    {
        width: 4PX;
        height: 0;
    }
    /*定義滑塊 內陰影+圓角*/
    .demo-XL .demo-chooseOption-XL::-webkit-scrollbar-thumb
    {
        background: #CECECE;
        border-radius:2px;
        width: 4px;
    }
    .demo-XL .transparent-XL {
        opacity: 0.3;
    }
    .nonePeople-XL {
        text-align: center;
        padding: 10px 0;
        font-size:12px;
        color:rgba(216,216,216,1);
    }
</style>
複製代碼
相關文章
相關標籤/搜索