實現方式和遇到問題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>
複製代碼