vue TagsInput组件封装详解

618 阅读1分钟

简介:TagsInput是一种可通过键入检索值回车生成检索条件标签,回退键删除上一个标签。效果如下:

QQ录屏20230320153043.gif

  1. 基础样式

2023-03-20_152648.png

<template>
  <div class='custom_input'>
    <div style="width:100%;"
         ref="searchP">
      <div class="ipt_style">
        <div class="ipt_left">
          <div class="ipt_container">
            <span class='btn'
                  v-for='(tag, index) in tags'
                  :key='index'>
              <i class="search_val"
                 :style="{'maxWidth': Number(poverWidth) - (tag.length * 14 + 16 + 75) + 'px'}">{{tag.value}}</i>
              <i class="el-icon-error search_close"
                 @click="closeTag(index)"></i>
            </span>
            <input type="text"
                   ref='input'
                   class="el-select__input"
                   @focus="showOptions"
                   :placeholder="placeholder"
                   @blur="closeOptions"
                   @keyup.delete="remove"
                   style="flex-grow: 1; width: 0.0961538%;height:32px;lineHeight:32px;"
                   @keyup.enter="add"
                   v-model='current'>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>
<script>
export default {
  name: 'CustomInput',
  data () {
    return {
      current: '',
      placeholder: '请输入检索内容!'
    }
  }
}
</script>
<style lang="less">
.custom_input {
  .ipt_style {
    border: 1px solid #dcdfe6;
    background-color: #ffffff;
    border-radius: 5px;
    width: 100%;
    position: relative;
    .ipt_left {
      display: inline-block;
      width: 100%;
    }
    .operation {
      display: inline-block;
      width: 60px;
      padding-left: 6px;
      position: absolute;
      right: 0px;
      top: 50%;
      margin-top: -10px;
    }
    .operation i {
      font-size: 20px;
      margin: 0 2px;
      cursor: pointer;
    }
  }
  .ipt_container {
    z-index: 1;
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    line-height: normal;
    white-space: normal;
    .el-select__input {
      border: none;
      outline: none;
      padding: 0;
      margin-left: 15px;
      color: #666;
      padding-right: 10px;
      font-size: 14px;
      appearance: none;
      height: 28px;
      background-color: transparent;
    }
  }
}
</style>
  1. 事件
<input type="text"
   ref='input'
   class="el-select__input"
   @focus="showOptions"
   :placeholder="placeholder"
   @blur="closeOptions"
   @keyup.delete="remove"
   style="flex-grow: 1; width: 0.0961538%;height:32px;lineHeight:32px;"
   @keyup.enter="add"
   v-model='current'>

// 聚焦点事件
showOptions () {
  this.removeFlg = 2 // 删除标签
}
// 失焦事件
closeOptions () {
  this.visible = false // 隐藏下拉选择区域
}
// 退格事件
remove (e) {
  // 退格时判断input中是否有值,若有值正常删除,若没值则判断tags,将最后一个tags删除一个字符并放入input
  const val = e.target.value
  if (val === '' && this.tags.length > 0) {
    this.removeFlg++
  }
  if (this.removeFlg > 1 && this.tags.length > 0) {
    const length = this.tags.length - 1
    this.current = this.tags[length].value
    this.tags.splice(length, 1)
    this.removeFlg = 0
  }
}
// 回车事件
add (e) {
  // 回车获取输入值,若输入值中有空格则代表“或”条件
  const val = e.target.value
  if (!val) return
  const arr = val.split(' ')
  let tempStr = ''
  if (val.length > 1) {
    arr.forEach((element, index) => {
      if (arr.length - 1 === index) {
        tempStr += element
      } else {
        tempStr += element + '或'
      }
    })
  } else {
    tempStr = arr[0]
  }
  const temp = {
    value: tempStr,
    keyValue: arr,
    op: arr.length > 1 ? 'or' : 'and'
  }
  this.tags.push(temp)
  this.current = ''
}

3.删除标签事件及标签内容超长处理

微信截图_20230320152739.png

/* 遍历tags数据,展示生成的查询tag */
<span class='btn' v-for='(tag, index) in tags' :key='index'>
  <i>{{tag.label}}</i>
  /* 此处处理输入值超长,超出input框。poverWidth为下拉选择框的整体宽度减去tag字段名的宽度减去图标、padding距离等,剩余宽度则为实际输入内容区宽度,超出此宽度则...省略 */
  <i class="search_val" :style="{'maxWidth': Number(poverWidth) - (16 + 75) + 'px'}">{{tag.value}}</i>
  <i class="el-icon-error search_close" @click="closeTag(index)"></i>
</span>


// tag悬浮展示删除按钮,点击时删除当前tag
closeTag (index) {
  this.tags.splice(index, 1)
}
  1. 完整代码
<template>
  <div class='custom_input'>
    <div style="width:100%;"
         ref="searchP">
      <div class="ipt_style">
        <div class="ipt_left">
          <div class="ipt_container">
            <span class='btn'
                  v-for='(tag, index) in tags'
                  :key='index'>
              <i class="search_val"
                 :style="{'maxWidth': Number(poverWidth) - (tag.length * 14 + 16 + 75) + 'px'}">{{tag.value}}</i>
              <i class="el-icon-error search_close"
                 @click="closeTag(index)"></i>
            </span>
            <input type="text"
                   ref='input'
                   class="el-select__input"
                   @focus="showOptions"
                   :placeholder="placeholder"
                   @blur="closeOptions"
                   @keyup.delete="remove"
                   style="flex-grow: 1; width: 0.0961538%;height:32px;lineHeight:32px;"
                   @keyup.enter="add"
                   v-model='current'>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>
<script>
export default {
  name: 'CustomInput',
  data () {
    return {
      current: '',
      placeholder: '',
      tags: [],
      poverWidth: 0,
      removeFlg: 0
    }
  },
  props: {
  },
  watch: {
    tags: {
      handler (val) {
        if (val.length) {
          this.placeholder = ''
        } else {
          this.placeholder = '请输入检索内容!'
        }
      },
      deep: true,
      immediate: true
    }
  },
  mounted () {
    this.poverWidth = this.$refs.searchP.offsetWidth
  },
  methods: {
    closeTag (index) {
      this.tags.splice(index, 1)
    },
    showOptions () {
      this.removeFlg = 2
    },
    closeOptions () {
      this.visible = false
    },
    addHandle (key, label, val) {
      const temp = {
        key: key,
        label: label,
        value: val
      }
      this.tags.push(temp)
      this.current = ''
      this.visible = false
    },
    add (e) {
      const val = e.target.value
      if (!val) return
      const arr = val.split(' ')
      let tempStr = ''
      if (val.length > 1) {
        arr.forEach((element, index) => {
          if (arr.length - 1 === index) {
            tempStr += element
          } else {
            tempStr += element + '或'
          }
        })
      } else {
        tempStr = arr[0]
      }
      const temp = {
        value: tempStr,
        keyValue: arr,
        op: arr.length > 1 ? 'or' : 'and'
      }
      this.tags.push(temp)
      this.current = ''
      this.visible = false
    },
    remove (e) {
      const val = e.target.value
      if (val === '' && this.tags.length > 0) {
        this.removeFlg++
      }
      if (this.removeFlg > 1 && this.tags.length > 0) {
        const length = this.tags.length - 1
        this.current = this.tags[length].value
        this.tags.splice(length, 1)
        this.removeFlg = 0
      }
    }
  }
}
</script>
<style lang="less">
.custom_input {
  .elPopover {
    .tips {
      color: #8c8e91;
    }
  }
  .ipt_style {
    border: 1px solid #dcdfe6;
    background-color: #ffffff;
    border-radius: 5px;
    width: 100%;
    position: relative;
    .ipt_left {
      display: inline-block;
      width: 100%;
    }
    .operation {
      display: inline-block;
      width: 60px;
      padding-left: 6px;
      position: absolute;
      right: 0px;
      top: 50%;
      margin-top: -10px;
    }
    .operation i {
      font-size: 20px;
      margin: 0 2px;
      cursor: pointer;
    }
  }
  .option_value {
    width: 100%;
    white-space: nowrap;
    overflow: hidden; //文本超出隐藏
    text-overflow: ellipsis; //文本超出省略号替代
  }
  .hoverSty {
    background-color: #f5f7fa;
  }
  .ipt_container {
    z-index: 1;
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    line-height: normal;
    white-space: normal;
    .btn {
      display: inline-block;
      border: 1px solid #4c7dff;
      border-radius: 2px;
      padding: 0;
      margin: 4px 0px 4px 15px;
      position: relative;
      i {
        display: inline-block;
        padding: 0 4px;
        font-style: normal;
        font-size: 14px;
      }
      i:first-child {
        background-color: #4c7dff;
        color: white;
      }
      .search_val {
        line-height: 20px;
        color: #4c7dff;
        white-space: nowrap;
        vertical-align: top;
        overflow: hidden; //文本超出隐藏
        text-overflow: ellipsis; //文
      }
      .search_close {
        font-size: 12px;
        position: absolute;
        right: -6px;
        top: -6px;
        padding: 1px;
        background-color: white;
        border-radius: 50%;
        display: none;
      }
    }
    .btn:hover .search_close {
      display: inline-block;
    }
    .el-select__input {
      border: none;
      outline: none;
      padding: 0;
      margin-left: 15px;
      color: #666;
      padding-right: 10px;
      font-size: 14px;
      appearance: none;
      height: 28px;
      background-color: transparent;
    }
  }
}
</style>

5.引入(组件查询值的传入传出等暂未做处理,自己可根据实际情况做传参、事件等)

// 使用
<cus-input></cus-input>
// 引入
import CusInput from './customInput'
// 注册
components: {
  CusInput
}