1. 先弄明白el-select的键盘交互机制:为什么回车不会收起下拉框
做后台管理系统的时候,很多同事都跟我抱怨过同一个问题:在el-select里用键盘选完值,习惯性地按一下回车,结果发现下拉面板纹丝不动地挂着,要么就自己从"选中状态"又弹出来了,表单怎么都提交不上去。
一开始我也以为是ElementUI的bug,后来翻了几遍源码,才意识到这根本不是缺陷,而是组件的交互模型和我们日常的"回车即确认"直觉有冲突。理解了这个模型,后面所有的"隐藏下拉菜单""触发键盘事件"就都有了解法。
1.1 下拉面板的显示状态:组件内部的visibleState到底由谁控制
el-select组件内部维护了一个叫visibleState的布尔值,它决定了下拉面板是否渲染、是否展开。这个值不是只受点击影响的,它同时被以下几种信号驱动:
- 鼠标点击输入框/触发器时置为
true,点击外部区域、选择完选项时会被置回false,这是最常见的方式。 - 键盘操作时,
visibleState会被独立处理。具体来说,在组件根节点上绑定了keydown事件,当按下Enter时,组件内部逻辑首先是"让当前高亮项生效",而不是切换面板开合状态。 visibleState还受disabled、远程搜索的loading、filterable清空状态影响,某些状态下即使你调用了显示方法,面板也会被强制收起。
我用一个通俗类比来解释:el-select把"回车"和"点击"这两个动作绑定到了同一个回调上。你点鼠标时,点击事件天然会触发失焦(blur),blur又会把面板关掉,所以看起来"点击一下就把面板关了";但回车不会让输入框失焦,所以组件认为你还在面板里继续操作,自然不会主动关。
这就是为什么你按回车想"确认并提交",但面板仍然赖着不走。
1.2 回车键在组件内部的实际处理链路:从keydown到option的click
要解决"回车隐藏下拉菜单"的问题,必须先搞清楚el-select里回车键被分发到了哪里。
在ElementUI 2.x版本的源码中,el-select根节点上绑定了keydown事件处理函数,在这个处理函数内部有一个非常关键的switch判断:
// 简化自 ElementUI 内部逻辑 handleKeydown(event) { const { keyCode } = event; // 删除、方向键对 highlightOption 进行上下移动 if (keyCode === 38 || keyCode === 40) { this.highlightOption(event); return; } // Enter 键:触发已选中(高亮)选项的点击逻辑 if (keyCode === 13) { if (this.visibleState && this.highlightIndex >= 0) { this.options[this.highlightIndex].click(); event.preventDefault(); } } }说几个容易被忽略的细节:
options[this.highlightIndex].click()并不是原生DOM的click,而是组件内部封装的方法,它会触发一次"当前项被选中"的动作。- 这个动作最终会调用
handleOptionSelect,把选中值写进绑定的v-model,然后大多数情况下会收起面板——注意,是大多数情况。你如果设置了multiple多选模式,回车选择完一项时面板不会关闭,这是刻意设计,方便连续选择多项。 - 如果你的下拉选项中某个option设置了
disabled,回车作用在它上面时不会触发任何关闭逻辑,你会觉得"回车键失灵了"。 - 如果当前没有高亮项(
highlightIndex === -1),回车按键会被组件吞掉,既不会触发选中,也不会收起面板,也不会向上冒泡到你的业务代码里。
明白了这个链路,你就该知道:让回车隐藏下拉菜单,本质上是要抢在组件内部逻辑之前处理回车事件,或者在组件选中动作完成之后主动去收面板。
2. 让回车真正"听话":隐藏下拉菜单的几种可行路径
针对"回车时隐藏下拉菜单"这个诉求,我试过好几种方案,有简单粗暴的,也有相对优雅的。先给结论:没有一种方案是银弹,取决于你的业务场景是"回车提交表单"还是"回车仅确认选择"。下面把我验证过的路径列出来,并标注适用场景。
2.1 方案一:手动触发blur事件——最自然但最容易翻车
原理很简单:el-select在失去焦点时会自动把visibleState置为false,所以只要你能让输入框主动失焦,面板就收了。
<el-select ref="mySelect" v-model="value" @keydown.enter.native="handleEnter" > <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" /> </el-select>handleEnter() { // 基于常见实践的补充:利用原生 DOM 能力触发放弃焦点 this.$refs.mySelect.$refs.input.blur(); // 后续处理你自己的键盘事件,比如提交表单 this.confirmByEnter(); }这个方案在单选、非filterable场景下实测是有效的——$refs.input指向组件内部的input元素,对它调用blur()会导致整个select组件失焦,进而收起面板。
但这方案有几个坑,我逐一说明:
- 如果你用的是filterable可搜索模式,
$refs.input是你的搜索输入框。回车之后你虽然调用了blur,但搜索框在失焦瞬间可能触发@blur事件,此时如果下拉列表还有残余文本,会先触发清空逻辑,导致你刚选中的值看起来"被清掉了"。实际测下来不是值被清掉,而是显示名来回闪了一下,体验很差。 - 如果你是这个select的唯一下拉组件,直接blur没什么问题;但如果页面里有其他逻辑依赖这个select的
focus状态(比如根据焦点显示联动信息),blur会误触联动逻辑。 - 某些内部版本ElementUI中,
this.$refs.mySelect.$refs.input可能不是输入框而是触发器元素,直接调用blur()会报错。稳妥的做法是先判断$refs.input && $refs.input.blur类型。
所以我现在的习惯是:blur()方案只用于最简单、单一、无搜索框的select场景。
2.2 方案二:间接驱动visibleState——更可控但不推荐直接操作内部状态
既然visibleState决定面板开合,那能不能直接改组件的visibleState?能,但不建议。原因是ElementUI的组件状态更新并不是所有字段都能响应式驱动UI,直接改visible相关内部数据,在2.15.x版本里确实有效,但依赖内部命名属于"贴地飞行",升级小版本有可能失效。
一个更可控的间接做法是:通过调用组件暴露的focus方法、再立刻触发别的操作,让组件以为自己失焦。这个方法比较绕,实现如下:
handleEnter() { const select = this.$refs.mySelect; // 重点:先让组件重新聚焦,再在下一帧强制让它失焦,达到“收起但不改变选中值”的效果 if (select.focused) { select.$refs.input.blur(); } // 你的业务逻辑 this.confirmByEnter(); }这种做法的本质还是借用blur,只是加了一层"确保当前处于聚焦态"的判断,避免重复调用blur引发的异常。
真正能精准控制面板开合的,是在模板上绑定的visible属性,注意这个visible是ElementUI 2.15+版本中el-select支持的一个属性,用于手动控制下拉面板的显示隐藏:
<el-select v-model="value" :visible="dropdownVisible" @keydown.enter.native="handleEnter" @visible-change="(val) => dropdownVisible = val" >handleEnter() { // 基于常见实践的补充:面板展开时,先主动将 visible 置为 false,再执行业务逻辑 this.dropdownVisible = false; this.confirmByEnter(); }这是个非常干净的方案,适合新版ElementUI。但有个前提:visible属性第一次生效需要组件完成初始化,如果你在组件mounted之前就设置了true,会出现面板初始状态异常的情况。另外,当你写:visible时,组件内部会认为面板的开关完全由外部控制,这时鼠标点击输入框将不再自动展开面板——你必须在点击事件里自己设置dropdownVisible = true。等于把开关逻辑全接管过来了,工作量反而变大了。
2.3 方案三:模拟点击触发器——最接近用户手动操作的"伪装"方案
这个方法是从一个老前端同事那学来的:el-select的触发逻辑本质上绑在"点击"事件上,如果能让组件内部"认为"自己经历了点击+失焦,面板自然就收起了。实现方式是在回车事件里依次触发$refs.select.handleBlur()或者触发触发器元素的click再blur。但我实测之后认为这个方案太依赖组件内部方法,不推荐在新的代码里使用,因为组件内部方法名在压缩混淆后不稳定。
综上,我把目前可用的方案整理成表格,方便你对照选型:
| 方案 | 实现难度 | 适用范围 | 稳定性 | 额外注意点 |
|---|---|---|---|---|
| 手动blur | 低 | 单选、无filterable的简单表单场景 | 中 | 对搜索/清空逻辑有干扰 |
| 外部visible控制 | 中 | 新版ElementUI(2.15+) | 高 | 必须接管点击展开逻辑 |
| 模拟触发器点击 | 中 | 老项目临时救急 | 低 | 依赖内部方法名,不推荐 |
| 监听键盘并回调业务函数 | 低 | 所有场景 | 高 | 需要配合上面任一种收面板方案 |
3. 回车触发自定义键盘事件的完整实现:带可复现的代码样例
方案确认之后,最核心的问题就变成了:在收面板的同时,怎么触发属于你自己的键盘事件逻辑。比如回车确认提交、回车跳转下一步、回车搜索等。
这一节我直接给出一份可复现代码,并解释每段代码的设计意图。
3.1 场景设定与代码结构
假设这样一个业务场景:某个订单编辑页面里,用户可以从下拉框选择"配送方式",选完后必须按回车确认,系统自动把确认结果提交到服务端,同时关闭下拉面板。
为了避免冲突,我把"组件自身的回车选中行为"和"业务确认行为"彻底分开:
<template> <el-select ref="deliverySelect" v-model="deliveryType" class="delivery-select" :filterable="false" @keydown.enter.native.prevent="handleDeliveryEnter" @change="handleDeliveryChange" @visible-change="handleVisibleChange" > <el-option v-for="item in deliveryOptions" :key="item.type" :label="item.label" :value="item.type" /> </el-select> </template>这里有个常被忽略的关键点:@keydown.enter.native.prevent。由于el-select是组件,普通的@keydown.enter并不会绑定到原生DOM上,必须加.native修饰符才能监听到。同时加.prevent的作用是阻止事件继续传递给组件内部的keydown处理逻辑——这一步是重中之重,如果不阻止默认行为,组件内部会在按下回车时执行"高亮项选中"而非你的业务逻辑。
3.2 业务处理函数:收面板 + 取当前高亮项 + 触发确认事件
data() { return { deliveryType: '', deliveryOptions: [ { type: 'express', label: '快递配送' }, { type: 'self_pick', label: '门店自提' }, { type: 'same_city', label: '同城急送' } ], currentHighlightIndex: -1, panelOpened: false }; }, methods: { // 回车主处理逻辑 handleDeliveryEnter(event) { // 1. 收起面板——手动blur方案(此处适用于无filterable的单选) this.$refs.deliverySelect.$refs.input.blur(); // 2. 触发键盘事件后续业务逻辑 // 这里根据组件当前选中值和面板状态决定行为 if (this.panelOpened) { this.confirmByEnter(); } else { // 面板本身就是关闭状态,说明用户把select当成一次性提交按钮 this.submitAfterEnter(); } }, // 业务确认逻辑 confirmByEnter() { // 业务侧操作,例如提交表单或者查询列表 console.log('回车确认,当前值:', this.deliveryType); this.submitDelivery(this.deliveryType); }, submitAfterEnter() { console.log('面板未打开,直接提交'); this.submitDelivery(this.deliveryType); }, submitDelivery(type) { // 模拟提交:实际项目里这里会调用API this.$message.success(`已确认配送方式:${type}`); }, handleDeliveryChange(val) { // 值变化时,如果面板还开着,按回车应该要触发confirm console.log('选中值变化为:', val); }, handleVisibleChange(val) { this.panelOpened = val; } }3.3 为什么这个实现能同时满足"隐藏菜单"和"触发键盘事件"
这段代码的核心有两点:
第一,用@keydown.enter.native.prevent把回车事件从组件内部逻辑里"抢"了过来。因为.prevent会调用event.preventDefault(),组件内部监听到的回车事件虽然也触发了,但不会执行options[highlightIndex].click()。此时组件的选中状态不会被改变,面板也不会自己关闭。
第二,主动调用blur()来触发组件自身的失焦收起机制。这一步等于把"回车确认"和"点击外部区域关面板"两种路径统一了。面板收起后,组件触发visible-change事件,handleVisibleChange会把panelOpened置为false,进而后续的键盘事件逻辑可以依据这个状态做分支。
你可以把整个流程想象成:你自己按下了一个"组合开关"——先关掉组件默认的回车行为,再触发组件的失焦行为,最后执行自己的业务回调。三件事互相隔离但又顺序清晰。
3.4 如果你必须支持多选模式,代码要改哪里
多选模式(multiple)下直接blur会导致已选中的tag被意外收起,而且多选状态下回车选中当前项后组件本来就不关闭面板,这是为了方便连续选择。如果你希望多选状态下回车确认并关闭:
handleDeliveryEnter(event) { // 多选场景:阻止默认行为,强制关闭面板,但不影响已选中的tags event.preventDefault(); // 多选时 $refs.input 可能不指向真实输入框,所以更稳妥的是调用 select 身上的 visible 控制 this.$refs.deliverySelect.visible = false; // 基于常见实践的补充,不推荐直接改内部属性,仅供参考 // 业务逻辑照常 this.confirmByEnter(); }但多选场景下我更建议使用外部控制visible方案,因为多选tag的显示是独立的,面板关闭不关闭和选中值没有必然联系,外部控制更清晰。下面会讲。
4. 实测中容易踩的四个坑:enter被吞、高亮丢失、异步选项、面板不收起
真实项目里,很少有一上来就顺顺利利跑通的场景。我列几个自己踩过、也帮同事排查过的典型问题,每个都给出完整的定位链路和解决方案,方便你对号入座。
4.1 坑一:回车事件被组件"吞"了,业务代码根本没执行
现象:绑定了@keydown.enter.native,但回调不触发;面板倒是自己收了,业务逻辑一点反应都没有。
定位过程:
- 我先在回调里加了
console.log,确认没打印。 - 然后检查是不是被
@keydown.enter(没有.native)绑定了——这是最容易被忽视的,el-select本身不是原生input,不加.native时Vue会把事件当成组件自定义事件,而组件内部并没有keydown-enter这种事件定义,所以回调永远不触发。 - 改成
.native后回调触发了,但执行得太晚——组件内部先一步处理了回车并选中了当前项,再轮到我们的回调时,面板状态已经变了。
解决方案:
<!-- 正确的写法:.prevent 优先阻断组件的内部行为 --> <el-select @keydown.enter.native.prevent="handleEnter">如果你既想触发自己的逻辑,又希望选中当前高亮项(即"回车既选中又确认"),就不能用.prevent,而应该去掉.prevent,改成在@change里做确认逻辑:
<el-select @keydown.enter.native="handleEnter" @change="handleChange">这种情况下组件内部会把高亮项选中并触发change,你在handleChange里确认选中即可,而handleEnter里只负责关闭面板。
定位这类问题最有效的方式是:在浏览器控制台里给el-select组件实例打console.log(this.$refs.mySelect),把$listeners、$attrs展开看看事件有没有被正确绑定。如果是$listeners里没有keydown,说明绑定方式不对;如果keydown在但回调不触发,说明事件被内部逻辑拦截了。
4.2 坑二:回车时设置了不存在的"当前高亮项"导致选择无效
现象:下拉面板打开,键盘上下选择项,高亮状态看起来移动到了某个选项上,但一回车,选中的却是上一次的值,而不是当前高亮的值。
定位过程:
- 检查
highlightIndex的值,发现它没跟随鼠标hover更新。 - 再看组件的
@mouseenter事件,发现选项上的hover事件并未触发,因为el-option组件在鼠标enter时是通过hoverIndex来记录的,但当鼠标在某个选项上时,键盘的上下键操作不会重置hoverIndex,这就导致键盘高亮(highlightIndex)和鼠标悬停(hoverIndex)两个状态互相干扰。 - 回车的选中逻辑是基于
hoverIndex还是highlightIndex?实测结论:ElementUI 2.x中,组件优先使用highlightIndex,但如果highlightIndex为-1而hoverIndex有值时,click()动作会作用在hoverIndex对应的选项上。所以当你键盘上下选择时,highlightIndex更新了,但hoverIndex还停留在鼠标最后停留的位置上,如果回车时组件内部取的是hoverIndex,就会导致"选中了鼠标悬停项而不是键盘高亮项"。
这个坑多发生在鼠标和键盘混用的操作场景下。解决办法有两个:
- 在
@keydown.enter.native回调里,手动根据方向键状态设置hoverIndex = highlightIndex,但因为是组件内部数据,操作起来并不优雅。 - 更推荐的做法:直接放弃组件默认回车选中逻辑,用
@change来同步高亮项和选中项,自己维护键盘导航。这相当于把键盘行为从组件内部"接管"出来。
我实际采用的折中方案是:监听方向键事件,在按下方向键后主动把鼠标hover状态清除:
handleKeydownNav(event) { if (event.keyCode === 38 || event.keyCode === 40) { // 键盘操作时清除鼠标hover影响,避免回车选中错位 this.$refs.deliverySelect.hoverIndex = -1; } }这个方案虽然依赖内部字段,但在2.x版本中已经稳定运行很长时间。
4.3 坑三:远程搜索 + 异步选项时,回车收面板后选中的值丢失
现象:使用filterable+remote远程搜索时,回车确认后下拉面板收起,但输入框显示的值和v-model不一致,或者重新打开面板时选中项消失了。
定位过程:
- 查看网络请求日志,发现回车确认后,远程搜索接口还在等待响应。
- 此时
v-model已经更新为回车选中的值,但由于搜索关键字还停留在输入框里,组件内部的selectedLabel会先显示关键字而不是选中项的名称。 - 我手动blur收面板时,搜索框会触发清空逻辑,但此时远程搜索的结果还没返回,组件认为当前没有匹配项,就把选中的标签重置了。
解决方案是:在远程搜索场景下,不要依赖blur来收面板。改用外部控制visible的方案,并且在visible-change回调里延迟清空搜索关键字:
handleVisibleChange(val) { if (!val) { // 基于常见实践的补充:延迟清理搜索关键字,等待远程数据返回后再重置 setTimeout(() => { this.$refs.deliverySelect.query = ''; }, 100); } }query是el-select组件内部用来存储搜索关键字的字段,你也可以用官方文档里的filter-method配合自定义状态来管理搜索词。
4.4 坑四:面板是否收起的效果在你自己的DOM里看不出来
现象:代码逻辑看着没问题,blur()也调了,但页面上面板并没有收起来。而且只在部分浏览器(旧版Edge、某些内嵌WebView)里这样。
定位过程:
- 检查浏览器控制台报错,确认
$refs.input存在,调用没报错。 - 发现这些浏览器中,
blur()是同步执行的,但组件内部的visibleState更新是异步的($nextTick之后)。如果在同一个事件循环里马上又让组件重新获得焦点(比如回车后自动聚焦到下一个输入框),focus事件会先把visibleState置为true,然后又因为blur置为false,最终两个异步操作叠加,状态冲突导致面板卡在打开状态。 - 进一步测试,在回车事件里调用
event.preventDefault()之后,如果后面紧接着同步调用了this.$nextTick(() => ...)重新聚焦,才会稳定复现。
解决方案是在blur和后续聚焦之间加一个微任务间隔,让组件内部的visibleState先稳定到关闭状态:
handleEnter() { this.$refs.deliverySelect.$refs.input.blur(); // 在下一帧重新聚焦到目标元素,避免blur和focus在同一个事件循环里打架 this.$nextTick(() => { this.$refs.targetInput.focus(); }); this.confirmByEnter(); }这个坑其实反映了所有异步UI组件的通性:手动调用组件暴露的方法时,要给它一个状态稳定窗口,不要在同一事件循环里连续做两个互斥的UI操作。这个经验在Vue里对其他组件也适用,比如el-popover、el-tooltip的显示切换,都有类似的异步状态更新逻辑。
5. 从ElementUI到Element Plus:回车行为的差异与适配
如果你维护的项目正在从Vue2 + ElementUI迁移到Vue3 + Element Plus,回车处理这块有几个差异值得提前注意。我有一次迁移后才发现,原本跑得好好的回车逻辑在Element Plus里完全失效了,最后定位到的原因是组件内部命名和事件分发机制都变了。
5.1 Element Plus的回车处理:toggleMenu触发源变更
Element Plus重写了下拉组件的键盘控制逻辑,不再优先走options[highlightIndex].click()这条路。在Element Plus中,回车键的逻辑是先检查当前是否有高亮项,如果高亮则触发selectOption,否则把toggleMenu置为false,从而只关闭面板而不做选择。
这个差异带来两个直接影响:
- 原来在ElementUI里"回车选中但面板不关"的行为,在Element Plus里变成了"回车选中并主动关面板"。如果你的业务代码在ElementUI里靠手动blur关面板,迁移后会出现重复关闭,一般不会报错但会触发额外的
visible-change事件,导致一些监听逻辑重复执行。 - Element Plus中
$refs.select.$refs.input不再是输入框实例,而是整个组件根节点。直接调用blur()不会有任何作用,因为新的根节点不是focusable的节点。
如果你在做适配,建议把收面板逻辑统一从"手动blur"改成"外部visible属性控制",这样在Element Plus上能继续正常工作,而且Element Plus对visible属性的定义更加明确。
5.2 同一套逻辑在Element Plus下的改造示例
<el-select ref="deliverySelect" v-model="deliveryType" :visible="dropdownVisible" @keydown.enter.prevent="handleDeliveryEnter" @visible-change="handleVisibleChange" >data() { return { dropdownVisible: false, }; }, methods: { handleDeliveryEnter() { // Element Plus:直接控制visible,不再操作blur this.dropdownVisible = false; this.confirmByEnter(); }, handleVisibleChange(val) { this.dropdownVisible = val; }, toggleDropdown() { // 接管自动展开:点击输入框区域时手动打开 this.dropdownVisible = !this.dropdownVisible; } }同时在模板里给输入框区域加一个点击事件绑定toggleDropdown,否则用户无法通过鼠标展开面板。
5.3 迁移时的回归测试清单
如果你准备把项目迁移到Element Plus,以下几条建议直接保存下来,回头逐项验证:
- 键盘上下选择时,高亮项是否正常移动。
- 远程搜索时,回车确认后是否出现值丢失。
- 多选模式下回车是否会误关面板。
visible属性外置后,鼠标点击其他区域是否还能正常关闭面板。- 表单提交时按回车是否会错误地触发下拉面板的打开逻辑。
另外,Element Plus对键盘事件使用了Vue3的onKeydown绑定方式,如果你在模板中同时使用了.native修饰符,Vue3会直接编译报错。所有原来写了.native的键盘绑定都需要去掉,改成普通的@keydown,因为组件事件和原生事件在Vue3中的区别已经没以前那么大了。
6. 一个更通用的思路:自己封装一个"回车确认下拉选择框"
经历了几次踩坑后,我现在遇到类似需求,通常不只是改el-select的用法,而是直接把它的键盘行为封装成一个自定义组件。这样既能把"回车确认"这种业务语义固化下来,又能在未来迁移到别的组件库时少改很多调用方代码。
6.1 封装思路与API设计
我给这个封装取名"确认型选择框",对外暴露的核心接口很简单:
value:双向绑定值options:选项列表@confirm:回车确认事件@change:选中变化事件
组件内部做的事情只有三件:
- 在
@keydown.enter.prevent里阻止默认行为; - 关闭自身下拉面板;
- 触发
confirm事件并把当前选中的值一并传出去。
这个封装最大的价值在于:你把"回车是否允许改变选中值"这个策略决定权升到了父组件层级。以后不管底层组件库怎么改键盘行为,你只维护这一个封装内部逻辑,业务侧完全察觉不到。
6.2 代码示例(基于ElementUI 2.x)
<template> <el-select :value="value" :visible="visible" @input="handleInput" @visible-change="handleVisibleChange" @keydown.enter.native.prevent="handleEnter" @click.stop="toggleVisible" > <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" /> </el-select> </template>export default { name: 'ConfirmSelect', props: { value: { type: [String, Number], default: undefined }, options: { type: Array, default: () => [] } }, data() { return { visible: false }; }, methods: { handleInput(val) { this.$emit('input', val); }, handleVisibleChange(val) { this.visible = val; }, toggleVisible() { this.visible = !this.visible; }, handleEnter() { // 关闭面板 this.visible = false; // 触发业务确认事件 this.$emit('confirm', this.value); } } };实际使用时,页面里写起来非常简洁:
<confirm-select v-model="deliveryType" :options="deliveryOptions" @confirm="confirmHandler" @change="valueChangedHandler" />6.3 封装后的几个好处
封装之后,我在新项目里几乎不用再直接操作el-select的内部状态。原来那些手动blur、修改hoverIndex、控制query的代码全部收拢进了一个地方,各业务组件里只体现业务意图。而且这个封装对后续技术栈升级的影响面也最小——就算ElementUI换成了别的组件库,我只改封装内部,所有调用方照常运。
如果你也经常在项目里跟表单打交道,我个人建议尽早把这种"带确认语义"的通用交互组件沉淀下来,无论是不是用el-select,这种套路在Ant Design Vue的a-select、小程序的picker组件上同样适用。交互逻辑的抽象能力,才是这次踩坑经历换来的最大收获。