You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

自定义Vue Select组件的@blur事件异常问题求助

解决自定义自动完成组件的@blur与下拉项点击冲突问题

我明白你遇到的困境了——添加@blur事件后,点击下拉选项时菜单会提前关闭,导致选择操作失效;但去掉它又没法在点击外部时自动收起菜单。这其实是因为blur事件的触发时机早于click事件:当你点击下拉项时,输入框先触发blur关闭了菜单,下拉项的click事件就没机会执行了。

下面给你两种可行的解决方案:

方案一:使用延迟+点击取消的方式

这个方法的核心是给blur事件设置一个短暂延迟,让下拉项的click事件先执行,再决定是否关闭菜单。

修改步骤:

  1. 给组件最外层的.vselect容器添加ref:
<div class="vselect" ref="vselect">
  1. 在data里添加一个定时器变量:
data() {
  return {
    // ... 其他原有数据
    blurTimer: null
  }
}
  1. 修改onBlur方法,添加延迟:
onBlur: function () {
  // 设置延迟,让click事件先执行
  this.blurTimer = setTimeout(() => {
    this.toggled = false;
  }, 150);
}
  1. 修改handleItemClick方法,在点击时清除延迟:
handleItemClick: function (item) {
  clearTimeout(this.blurTimer); // 取消blur的关闭操作
  this.mutableValue = item;
  this.$emit('input', item);
  this.toggled = false;
}
  1. 额外添加全局点击监听(可选,增强健壮性):
mounted() {
  this.filterList = this.options;
  // 监听全局点击
  document.addEventListener('click', this.handleOutsideClick);
},
beforeUnmount() {
  // 组件销毁时移除监听,避免内存泄漏
  document.removeEventListener('click', this.handleOutsideClick);
},
methods: {
  // ... 其他原有方法
  handleOutsideClick(e) {
    if (this.$refs.vselect && !this.$refs.vselect.contains(e.target)) {
      this.toggled = false;
    }
  }
}

方案二:完全替换@blur为全局点击监听

这种方式更直接,去掉输入框的@blur事件,通过监听全局点击来判断是否关闭菜单,从根源上避免时序冲突。

修改步骤:

  1. 移除输入框的@blur="onBlur"事件
  2. 给组件外层容器添加ref(同方案一)
  3. 添加全局点击监听(同方案一的handleOutsideClick方法)
  4. 保留onFocus方法打开菜单,点击下拉项时手动关闭菜单

修改后的完整代码示例(方案一)

<template>
  <div class="vselect" ref="vselect">
    <div class="form-group">
      <label>{{label}}</label>
      <p class="control has-icon has-icon-right">
        <input 
          ref="selected" 
          @keyup="filterSelect($event)" 
          @focus="onFocus" 
          v-model="mutableValue" 
          type="text" 
          class="form-control" 
          @keydown.up.prevent="onKeyUp" 
          @keydown.down.prevent="onKeyDown" 
          @keydown.enter.prevent="onKeyEnter" 
          @blur="onBlur"
        >
      </p>
    </div>
    <div ref="dropdown" class="my-dropdown" v-show="toggled" v-if="options">
      <div 
        v-for="(item,index) in filterList" 
        :class="{'my-dropdwon--item':true,active:index === pointer}" 
        @click="handleItemClick(item)" 
        @mouseover="pointer = index"
      >{{item}}</div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'VSelect',
  props: {
    'options': Array,
    'label': String,
    'value': String,
  },
  data() {
    return {
      selected: null,
      toggled: false,
      filterList: [],
      mutableValue: null,
      mutableOptions: [],
      pointer: 0,
      blurTimer: null
    }
  },
  methods: {
    filterSelect: function (key) {
      if (!this.toggled) this.toggled = !this.toggled;
      let oldArr = this.options;
      if (this.mutableValue && this.mutableValue.length <= 0) 
        this.filterList = this.mutableOptions;
      else if (key.key.length == 1 || key.key == 'Backspace') {
        oldArr = oldArr.filter(item => {
          if (item.toLowerCase().includes(this.mutableValue.toLowerCase())) 
            return true;
        })
        this.filterList = oldArr;
      }
      this.$emit('input', this.mutableValue);
    },
    handleItemClick: function (item) {
      clearTimeout(this.blurTimer);
      this.mutableValue = item;
      this.$emit('input', item);
      this.toggled = false;
    },
    onFocus: function () {
      this.$refs.dropdown.scrollTop = 0;
      this.toggled = true;
    },
    onBlur: function () {
      this.blurTimer = setTimeout(() => {
        this.toggled = false;
      }, 150);
    },
    onKeyUp: function () {
      if (this.pointer > 0) this.pointer--;
      if (this.maybeAdjustScroll) {
        this.maybeAdjustScroll()
      }
    },
    onKeyDown: function () {
      // 修复指针越界问题:改为判断过滤后的列表长度
      if (this.pointer < this.filterList.length - 1) this.pointer++;
      if (this.maybeAdjustScroll) {
        this.maybeAdjustScroll()
      }
    },
    onKeyEnter: function () {
      if(this.filterList.length > 0) 
        this.handleItemClick(this.filterList[this.pointer])
      this.$refs.selected.blur();
      this.toggled = false;
    },
    maybeAdjustScroll() {
      let pixelsToPointerTop = this.pixelsToPointerTop()
      let pixelsToPointerBottom = this.pixelsToPointerBottom()
      if (pixelsToPointerTop <= this.viewport().top) {
        return this.scrollTo(pixelsToPointerTop)
      } else if (pixelsToPointerBottom >= this.viewport().bottom) {
        return this.scrollTo(this.viewport().top + this.pointerHeight())
      }
    },
    pixelsToPointerTop() {
      let pixelsToPointerTop = 0
      if (this.$refs.dropdown && this.$refs.dropdown.children) {
        for (let i = 0; i < this.pointer; i++) {
          pixelsToPointerTop += this.$refs.dropdown.children[i].offsetHeight
        }
      }
      return pixelsToPointerTop
    },
    pixelsToPointerBottom() {
      return this.pixelsToPointerTop() + this.pointerHeight()
    },
    pointerHeight() {
      let element = this.$refs.dropdown ? this.$refs.dropdown.children[this.pointer] : false
      return element ? element.offsetHeight : 0
    },
    viewport() {
      return {
        top: this.$refs.dropdown ? this.$refs.dropdown.scrollTop : 0,
        bottom: this.$refs.dropdown ? this.$refs.dropdown.offsetHeight + this.$refs.dropdown.scrollTop : 0
      }
    },
    scrollTo(position) {
      return this.$refs.dropdown ? this.$refs.dropdown.scrollTop = position : null
    },
    handleOutsideClick(e) {
      if (this.$refs.vselect && !this.$refs.vselect.contains(e.target)) {
        this.toggled = false;
      }
    }
  },
  mounted() {
    this.filterList = this.options;
    document.addEventListener('click', this.handleOutsideClick);
  },
  beforeUnmount() {
    document.removeEventListener('click', this.handleOutsideClick);
    clearTimeout(this.blurTimer);
  },
  watch: {
    value(val) {
      this.mutableValue = val
    },
    options(val) {
      this.mutableOptions = val
    },
    pointer() {
      this.maybeAdjustScroll()
    }
  }
}
</script>

<style scoped>
.vselect {
  display: block;
  position: relative;
}
.my-dropdown {
  width: 100%;
  background: #f7f7f7;
  margin-top: 0.1rem;
  border: 1px solid #ced4da;
  border-radius: 3px;
  transition: all 0.5s;
  position: absolute;
  z-index: 1;
  max-height: 10rem;
  overflow: auto
}
.my-dropdwon--item {
  padding: 0.5rem 1rem;
  cursor: pointer;
}
.my-dropdwon--item.active {
  background-color: #e9ecef;
}
</style>

另外我修复了onKeyDown方法里的一个小问题:原来的this.pointer < this.options.length改为this.pointer < this.filterList.length - 1,避免指针超出过滤后的列表范围。

内容的提问来源于stack exchange,提问作者Indrajeet Latthe

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.27 09:33:30