自定义输入搜索组件

本文档介绍如何创建一个自定义的输入搜索组件,该组件支持键值对输入,具有搜索功能,并能在用户按下回车时生成标签。通过提供代码实现部分,包括`edit-search.vue`和`test.vue`文件,帮助开发者理解并实现这一功能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

需求

  • 输入键值对 key="value"
  • 可搜索
  • 回车生成标签

效果图

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

代码实现

edit-search.vue

	<style scoped lang="scss">
  .edit-search {
    position: relative;
    &__container {
      position: relative;
    display: inline-flex;
    flex-wrap: wrap;
    align-items: flex-start;
    vertical-align: middle;
    width: 100%;
    height: auto;
    padding: 0 20px 0 10px;
    outline: none;
    background: $white;
    user-select: none;
    border: $border-base;
    border-radius: 4px;
    overflow: hidden;
    transition: border ease .2s, box-shadow ease .2s;
    &.error {
      border: 1px solid #f00;
    }
    .edit-search__tags {
      flex-shrink: 0;
      font-size: 12px;
      line-height: 12px;
      margin-right: 4px;
      margin-top: 4px;
      border-radius: 3px;
      border: $border-base;
      background: $bg-base;
      padding: 4px 6px;
      display: inline-block;
      &--close {
        font-size: 12px;
        margin-left: 5px;
        color: $text-placeholder;
        &:hover {
          color: $text-regular;
        }
      }
    }
    .edit-search__input {
      height: 30px;
      width: 0;
      min-width: 30px;
      flex: 1;
      color: $text-regular;
      cursor: default;

      &::placeholder {
        color: $text-placeholder;
      }

      &::-moz-placeholder {
        color: $text-placeholder;
        opacity: 1;
      }
    }
    }
    .change-label__popper {
      position: absolute;
      background: $white;
      box-shadow: 0 2px 12px 0 #0000000F;
      border: 1px solid #DCE2E6;
      border-radius: 4px;
      font-size: 14px;
      margin-top: 5px;
      left: 0;
      top: 27px;
      z-index: 10;
      cursor: auto;
      li {
        color: $text-secondary;
        line-height: 32px;
      }
    }
    .slid-enter-active,
      .slid-leave-active {
        opacity: 1;
        transform: scaleY(1);
        transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
        transform-origin: center top;
      }
      .slid-enter,
      .slid-leave-active {
        opacity: 0;
        transform: scaleY(0);
      }
  }
</style>
<template>
  <div class="edit-search">
    <div :class="['edit-search__container', {'error': errorEdit} ]">
      <div ref="tagRef">
        <div
          v-for="item in selectedTags"
          :key="item"
          class="edit-search__tags"
        >
          <span>{{ item }}</span>
          <i
            class="yi yi-fail-circle-fill edit-search__tags--close"
            @click.stop="handleTageRemove(item)"
          />
        </div>
      </div>
      <input
        ref="input"
        v-model="inputValue"
        type="text"
        placeholder="请输入"
        class="edit-search__input"
        @keydown.enter="handleEnter"
        @blur="handleBlur"
        @focus="focusInput"
      >
    </div>
    <transition
      v-if="type === 'value'"
      name="slid"
    >
      <ul
        v-if="showPopper"
        v-clickoutside="handleClose"
        class="change-label__popper"
        :style="'left: '+ leftSize +'px'"
      >
        <yv-scrollbox style="max-height: 300px; padding: 0 8px">
          <li
            v-for="(item, index) in keyOptions"
            :key="index"
            @click="selectOption(item)"
          >
            {{ item }}
          </li>
        </yv-scrollbox>
      </ul>>
    </transition>
  </div>
</template>
<script>
const clickoutside = {
  bind (el, binding, vnode) {
    function documentHandler (e) {
      if (el.contains(e.target) || e.target.className === 'edit-search__input') return false
      if (binding.expression) binding.value(e)
    }
    el._vueClickOut = documentHandler
    document.addEventListener('click', documentHandler)
  },
  update () {},
  unbind (el, binding) {
    document.removeEventListener('click', el._vueClickOut)
    delete el._vueClickOut
  }
}
export default {
  directives: { clickoutside },
  props: {
    value: {
      type: Array,
      default: () => []
    },
    type: {
      type: String,
      default: 'key'
    },
    options: {
      type: Array,
      default: () => []
    }
  },
  data () {
    return {
      inputValue: '',
      keyOptions: this.options,
      selectedOptions: [],
      selectedTags: [],
      errorEdit: false,
      showPopper: false,
      leftSize: 0
    }
  },
  watch: {
    options: {
      deep: true,
      handler: function (n) {
        this.keyOptions = n
        this.selectedOptions = this.selectedOptions.filter(item => n.indexOf(Object.keys(item)[0]) > -1)
        this.$nextTick(() => {
          this.leftSize = this.$refs.tagRef.offsetWidth + 5
        })
      }
    },
    selectedOptions: {
      deep: true,
      handler: function (n) {
        const selectedKey = []
        const selectTag = []
        if (this.type === 'key') {
          selectedKey.push(...n)
          selectTag.push(...n)
        } else {
          n.forEach(item => {
            selectedKey.push(Object.keys(item)[0])
            selectTag.push(Object.keys(item)[0] + '=' + Object.values(item)[0])
          })
        }
        this.selectedTags = selectTag
        this.keyOptions = this.options.filter(item => selectedKey.indexOf(item) === -1)
        this.$emit('input', n)
        this.$emit('change', n)
        this.$nextTick(() => {
          this.leftSize = this.$refs.tagRef.offsetWidth + 5
        })
      }
    },
    value: {
      deep: true,
      handler: function (n) {
        this.handlerVal(n)
      },
      immediate: true
    }
  },
  methods: {
    handlerVal (value) {
      if (this.type === 'key') {
        if (Array.isArray(value)) {
          this.selectedOptions.push(...value)
        } else {
          this.selectedOptions.push(value)
        }
      } else {
        this.selectedOptions = this.value
      }
    },
    handleClose () {
      this.showPopper = false
    },
    handleTageRemove (item) {
      if (this.type === 'key') {
        this.selectedOptions = this.selectedOptions.filter(em => em !== item)
      } else {
        this.selectedOptions = this.selectedOptions.filter(em => Object.keys(em)[0] !== item.split('=')[0])
      }
      this.showPopper = false
    },
    focusInput () {
      this.showPopper = true
    },
    selectOption (item) {
      this.inputValue = item + '='
      this.showPopper = false
    },
    handleEnter () {
      if (!this.inputValue) return
      if (this.type === 'key') {
        if (/^[a-zA-Z0-9\u4e00-\u9fa5_-]+$/.test(this.inputValue) && this.selectedOptions.indexOf(this.inputValue) === -1) {
          this.selectedOptions.push(this.inputValue)
          this.inputValue = ''
          this.errorEdit = false
        } else {
          this.errorEdit = true
        }
      } else {
        if (/^[a-zA-Z0-9_-]+=[a-zA-Z0-9\u4e00-\u9fa5_-]+$/.test(this.inputValue) && this.selectedOptions.indexOf(this.inputValue) === -1) {
          const inputVal = this.inputValue.split('=')
          const obj = {}
          obj[inputVal[0]] = inputVal[1]
          this.selectedOptions.push(obj)
          this.inputValue = ''
          this.errorEdit = false
          this.showPopper = false
          this.$refs.input.blur()
        } else {
          this.errorEdit = true
        }
      }
    },
    handleBlur () {
      this.errorEdit = false
      this.inputValue = ''
    }
  }
}
</script>

test.vue

	<style lang="scss" scoped>
  .search-class {
    width: 1000px;
  }
</style>
<template>
  <div class="page">
    <h3>键</h3>
      <edit-search
        ref="edit1"
        class="search-class"
        type="key"
        :value="edit1Val"
      />
      <yv-button @click="btnSelect">
        查询
      </yv-button>
      <h3 style="margin-top: 50px">
        键值对
      </h3>
      <edit-search
        ref="edit2"
        class="search-class"
        style="margin-bottom: 50px"
        type="value"
        :value="edit2Val"
        :options="selectOptions"
      />
      <yv-button @click="btnSelect">
        查询
      </yv-button>
	  <yv-button @click="edit2Val=[]">
        刪除
      </yv-button>
      <yv-button @click="btnAdd">
        加值
      </yv-button>
      <yv-button
        size="small"
        @click="changeOptions"
      >
        改變options
      </yv-button>
      <yv-button
        size="small"
        @click="changeSelect"
      >
        改變value
      </yv-button>
  </div>
</template>

<script>
import EditSearch from '../components/edit-search'
export default {
  name: 'Index',
  components: {
    EditSearch
  },
  data () {
    return {
      selectOptions: ['value1', 'value2', 'selectValue'],
      edit1Val: 'value1',
      // edit1Val: ['value11', 'value22'],
      edit2Val: [{ value1: '111' }, { value2: '222' }]
    }
  },
  methods: {
    btnSelect () {
      console.log(this.$refs.edit1.getValue())
      console.log(this.$refs.edit2.getValue())
    },
	btnAdd () {
      const arr = [{ value1: 'eee' }, { value3: 'eesss' }]
      this.edit2Val.push(...arr)
    },
    changeOptions () {
      this.selectOptions = ['value1', 'aaaaaaaaaa', 'ddddddddddd', 'bbbbbbbbb', 'eeeeeeeeeee', 'fffffffffff', 'bbbbbbbbrrrr']
    },
    changeSelect () {
      this.edit2Val.forEach(item => {
        item.value1 = 333
      })
    },
    changeVal (val) {
      this.edit2Val = val
    },
  }
}
</script>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值