记一个自定义下拉框组件,实现了单选、多选、过滤、树形结构等功能

337 阅读5分钟

不知各位使用 elementPlus 组件库中的 el-select 时有没有发现,每添加一个 el-select 组件,就意味着页面中同时会生成一个 el-popover 的下拉框组件,这样就会导致某些表单页面或者列表嵌套表单的页面大量的使用 el-select 组件时,页面中会生成大量的 el-popover 组件,导致页面卡顿,性能下降,那么有没有什么办法可以解决这个问题呢?

image.png

既然如此那就手动造一个全局下拉框组件,同时满足 单选,多选,树形下拉框,添加新item,搜索等功能,下面就开始我们的造轮子之旅吧!

ps: 此组件是基于 elementPlus 组件开发的,且项目中用到了 tailwindcssscss ,所以需要先安装这几个依赖。

目录结构如下,总共三个文件,两个组件和一个页面,其中 popover.vue 是弹出框组件,在一个页面中只需引入一个

├── demo
│   ├── components
│   │   ├── popover.vue
│   │   └── select.vue
│   └── index.vue
  1. popover.vue 负责弹出框组件
<template>
    <el-tooltip ref="tooltipRef" :visible="visible" effect="light" :placement="initPlacement" :virtual-ref="virtualRef" virtual-triggering transition="el-zoom-in-top" :gpu-acceleration="false" :show-arrow="config.showArrow" :popper-options="{ modifiers: [{ name: 'computeStyles', options: { adaptive: false, enabled: false } }] }" popper-class="!p-0" >
        <template #content>
            <div @click.prevent.stop class="box-border py-2" :style="popoverWidth">
                <div v-if="!options?.length" class="text-colors-3 text-center">
                    No Data
                </div>

                <el-scrollbar v-else max-height="336px">
                    <ul v-if="!config.isTree">
                        <li v-for="(item, index) in options" :key="`listItem-${index}`" @click="handleSelectPopoverItem($event, item)" class="flex justify-between items-center py-[5px] px-5 hover:bg-[#F5F6F7] cursor-pointer" :class="isChooseItem(item) && ['text-colors-primary', 'font-bold']">
                            <span class="whitespace-nowrap overflow-hidden text-ellipsis">{{ item[config.dataProps.label] }}</span>
                            <el-icon v-if="config.multiple && isChooseItem(item)" :size="12" class="flex-none"><Check /></el-icon>
                        </li>
                    </ul>
                    <div v-else>
                        <el-tree-v2 ref="treeRef" :data="options" :highlight-current="false" :props="config.dataProps" :height="200" :item-size="28" :expand-on-click-node="false" :filter-method="filterMethod" @node-click="handleNodeClick" class="px-4">
                            <template #default="{ node }">
                                <div class="w-full flex items-center justify-between pr-1 overflow-hidden">
                                    <p class="w-full overflow-hidden text-ellipsis whitespace-nowrap" :class="[ chooseData[config.dataProps.value] == node.data[config.dataProps.value] && '!text-colors-primary']">{{ node.data[config.dataProps.label] }}</p>
                                </div>
                            </template>
                            <template #empty>
                                <div class="h-[200px] leading-[200px] text-colors-3">No Data</div>
                            </template>
                        </el-tree-v2>
                    </div>
                </el-scrollbar>
            </div>
        </template>
    </el-tooltip>
</template>

<script setup>
import { Check } from '@element-plus/icons-vue';
const props = defineProps({
    virtualRef: { type: Object, default: () => ({}) },      // 虚拟触发元素【如果是ref,则传递 ref.value.$el】
    visible: { type: Boolean, default: false },             // 是否显示
})

const emit = defineEmits(['resetSelectPopoverConfig']);

onMounted(() => {
    document.addEventListener('click', handleResetSelectPopoverConfig);
})

const treeRef = ref(null);
const chooseData = ref(''); // 选择的数据
const config = ref({}); // 配置
// 非请求来的数据在弹窗显示时即可初始化数据
watch(() => props.visible, visible => visible && handleInitData());

// 计算popover宽度
const popoverWidth = computed(() => {
    return (config.value.fitInputWidth && props.virtualRef.offsetWidth) ? `width: ${props.virtualRef.offsetWidth}px;` : `width: ${config.value.popoverWidth || 400}px;`;
})

const tooltipRef = ref(null);
// 初始化数据
const initPlacement = ref('bottom');
const options = ref([]);
const handleInitData = () => {
    if (!props.visible) {
        config.value = {};
        return;
    } else {
        config.value = props.virtualRef.popoverConfig;
        nextTick(() => initPlacement.value = config.value.initPlacement || tooltipRef.value.popperRef.popperInstanceRef.state.placement);
        options.value = config.value.options ? JSON.parse(JSON.stringify(config.value.options)) : [];
        if (config.value.data) {
            // 赋值选中的数据
            chooseData.value = JSON.parse(JSON.stringify(config.value.data));

            // 树形结构的初始化展开
            if (config.value.isTree && config.value.data[config.value.dataProps.value]) {
                setTimeout(() => handleTreeExpand(config.value.data[config.value.dataProps.value]), 0);
            }
        } else {
            // 重置选中的数据
            chooseData.value = config.value.multiple ? [] : '';
        }
    }
}

// 树形结构展开
const handleTreeExpand = (key) => {
    const currentNode = treeRef.value && treeRef.value.getNode(key);
    if (!currentNode) return;
    const parantNode = currentNode.parent && treeRef.value.getNode(currentNode.parent.data[config.value.dataProps.value]);
    parantNode && treeRef.value.expandNode(parantNode, true);
    parantNode?.parent && handleTreeExpand(parantNode[config.value.dataProps.value]);
}

// 是否选中
const isChooseItem = computed(() => {
    return item => {
        if (config.value.multiple) {
            return chooseData.value.find(i => i[config.value.dataProps.value] === item[config.value.dataProps.value])
        } else {
            return chooseData.value[config.value.dataProps.value] === item[config.value.dataProps.value];
        }
    };
})

// 更新tooltip的显示,防止tooltip错位
const handleUpdatePopover = () => {
    setTimeout(() => tooltipRef.value.updatePopper(), 1);
}

// 选中项
const handleSelectPopoverItem = (e, item) => {
    if (config.value.multiple) {
        const index = chooseData.value.findIndex(i => i[config.value.dataProps.value] === item[config.value.dataProps.value]);
        index > -1 ? chooseData.value.splice(index, 1) : chooseData.value.push(item);
        config.value.onSelect(JSON.parse(JSON.stringify(chooseData.value)));
        handleUpdatePopover();
    } else {
        handleResetSelectPopoverConfig();
        if (item && chooseData.value[config.value.dataProps.value] === item[config.value.dataProps.value]) return;
        chooseData.value = item;
        config.value.onSelect(JSON.parse(JSON.stringify(item)));
    }
}
// 点击node节点
const handleNodeClick = (data, node, e) => {
    handleSelectPopoverItem(e, data);
}

// 过滤指定节点
const filterMethod = (query, node) => {
    // 当query为空时且非子节点时,折叠所有节点
    if (query === '' && node.children) {
        const trueNode = treeRef.value.getNode(node[config.value.dataProps.value]);
        trueNode && nextTick(() => treeRef.value.collapseNode(trueNode));
    }
    return node[config.value.dataProps.label].includes(query);
}

// 搜索
const handleSearch = (query) => {
    if (config.value.isTree) {
        treeRef.value && treeRef.value.filter(query);
        return;
    }
    if (query) {
        const regex = new RegExp(query, 'gi');
        options.value = config.value.options.filter(item => regex.test(item[config.value.dataProps.label]));
    } else {
        options.value = JSON.parse(JSON.stringify(config.value.options));
    }
    
    handleUpdatePopover();
}

// 重置配置
function handleResetSelectPopoverConfig() {
    emit('resetSelectPopoverConfig');
}

// 单独更新选中的值
const handleUpdateChooseData = (val) => {
    chooseData.value = JSON.parse(JSON.stringify(val));
}

onBeforeUnmount(() => {
    document.removeEventListener('click', handleResetSelectPopoverConfig);
})

defineExpose({
    handleInitData,
    handleUpdateChooseData,
    handleSearch,
    handleUpdatePopover,
    getVisibleStatus: () => props.visible
})
</script>

<style lang="scss" scoped>
:deep(.el-virtual-scrollbar) {
    right: -6px!important;
}

:deep(.el-tree) {
    .el-tree-node__content {
        gap: 4px;
        
        &>.el-tree-node__expand-icon {
            padding: 2px;
        }
    }

    .el-tree--highlight-current .el-tree-node.is-current>.el-tree-node__content {
        background-color: transparent;
    }
}
</style>
  1. select.vue 负责下拉选择组件
<template>
    <div ref="selectRef" @click.prevent.stop="handleTrigger" class="el-select">
        <div @mouseenter="handleHover($event, true)" @mouseleave="handleHover($event, false)" class="el-select__wrapper" :class="[currentPopoverTarget === selectRef && 'is-focused', isHovering && 'is-hovering', disabled && 'is-disabled']">
            <div class="el-select__selection">
                <div class="el-select__selected-item el-select__input-wrapper w-full flex gap-1">

                    <template v-if="filterable">
                        <input ref="filterInputRef" v-model="filterInputValue" :disabled="disabled" type="text" autocomplete="off" spellcheck="false" @input="handleFilterInput" @blur="handleFilterInputBlur" class="el-select__input flex-none" style="width: 11px;">
                        <span ref="filterInputSpanRef" aria-hidden="true" class="el-select__input-calculator">{{ filterInputValue }}</span>
                    </template>
                    
                    <template v-if="multiple">
                        <el-tag v-for="(tag, tagIndex) in data" :key="`tag-item-${tagIndex}`" disable-transitions :closable="!disabled" type="info" @close="handleCloseTag(tagIndex)" >{{ tag?.[dataProps?.label] }}</el-tag>
                    </template>
                    <span v-else-if="!filterInputValue" class="w-fit el-select__selected-item el-select__placeholder text-colors-1 break-all text-ellipsis line-clamp-1" :class="[disabled && 'text-colors-3', (!data?.[dataProps?.label] || filterInputIsFocus) && 'is-transparent']">{{ data?.[dataProps?.label] || placeholder }}</span>

                    <template v-if="addItems && multiple && !filterable">
                        <input ref="addItemsInputRef" v-model="addItemsInputValue" :disabled="disabled" type="text" autocomplete="off" spellcheck="false" @input="handleAddItemsInput" @blur="handleAddItemsInputBlur" class="el-select__input flex-none" style="width: 11px;">
                        <span ref="addItemsInputSpanRef" aria-hidden="true" class="el-select__input-calculator">{{ addItemsInputValue }}</span>
                    </template>
                </div>
            </div>
            <div class="el-select__suffix">
                <i v-if="clearable && isHovering && ((!multiple && data?.[dataProps?.label]) || (multiple && data.length))" @click.prevent.stop="handleClear" class="el-icon el-select__caret el-input__icon el-select__clear">
                    <el-icon><CircleClose /></el-icon>
                </i>
                <i v-else class="el-icon el-select__caret el-input__icon" :class="currentPopoverTarget === selectRef && 'is-reverse'">
                    <el-icon><ArrowDown /></el-icon>
                </i>
            </div>
        </div>
    </div>
</template>

<script setup>
import { CircleClose, ArrowDown } from '@element-plus/icons-vue';

const props = defineProps({
    modelValue: '',     // 双向绑定的值
    selectPopoverRef: null,     // select的popover [多选时需传递]
    options: null,            // 数据
    disabled: { type: Boolean, default: false },            // 是否禁用
    currentPopoverTarget: null,                           // 当前激活popover的target
    dataProps: { type: Object, default: () => ({ value: 'value', label: 'label', children: 'children' }) },      // 结构配置
    placeholder: { type: String, default: 'pleaseSelect' },
    fitInputWidth: { type: Boolean, default: true },      // 是否适应输入框宽度
    popoverWidth: { type: Number, default: 400 },      // popover宽度
    filterable: { type: Boolean, default: false },    // 是否可搜索
    clearable: { type: Boolean, default: false },    // 是否显示清除按钮
    multiple: { type: Boolean, default: false },    // 是否多选
    showArrow: { type: Boolean, default: true },     // 是否显示箭头
    addItems: { type: Boolean, default: false },      // 是否可以添加内容 [与filterable不可同时使用,优先filterable]
    initPlacement: { type: String, default: '' },      // 初始位置
    isTree: { type: Boolean, default: false },      // 是否为树形下拉框
});
const emit = defineEmits(['update:modelValue', 'click', 'change']);

watch([() => props.modelValue, () => props.options], () => {
    handleInitData();
});

onMounted(() => {
    // 绑定下拉框配置项
    handleInitConfig();
})

// 初始化配置
const selectRef = ref(null);
const handleInitConfig = () => {
    nextTick(() => {
        selectRef.value.popoverConfig = {};
        const keyList = ['isTree', 'dataProps', 'options', 'fitInputWidth', 'filterable', 'showArrow', 'multiple', 'popoverWidth', 'initPlacement'];
        keyList.forEach(key => selectRef.value.popoverConfig[key] = props[key]);
        selectRef.value.popoverConfig['onSelect'] = handleSelect;
        handleInitData();
    })
}
// 初始化选中数据
const data = ref(null);
const handleInitData = () => {
    if (!props.modelValue || (props.multiple && !props.modelValue?.length) || !props.options || !props.options.length) {
        if (props.multiple) {
            data.value = [];
            props.modelValue?.forEach(item => {
                const obj = {};
                obj[props.dataProps.label] = item;
                data.value.push(obj);
            })
        } else {
            data.value = {};
            data.value[props.dataProps.label] = props.modelValue;
        }
        return;
    }
    if (props.isTree) {
        let findItem = handleFindTreeItem(props.options, props.modelValue);
        if (findItem) {
            data.value = findItem;
        } else {
            data.value = {};
            data.value[props.dataProps.label] = props.modelValue;
        }
    } else {
        if (props.multiple) {
            const labelArr = [];
            props.modelValue.forEach(item => {
                const findItem = props.options.find(option => option[props.dataProps.value] === item);
                findItem ? labelArr.push(findItem) : labelArr.push({ [props.dataProps.value]: item, [props.dataProps.label]: item });
            })
            data.value = labelArr.length ? labelArr : props.modelValue;
        } else {
            const findItem = props.options.find(item => item[props.dataProps.value] === props.modelValue);
            if (findItem) {
                data.value = findItem;
            } else {
                data.value = {};
                data.value[props.dataProps.label] = props.modelValue;
            }
        }
    }
    selectRef.value.popoverConfig.data = data.value;
    selectRef.value.popoverConfig.options = props.options;
}
// 树形结构查找选中项
const handleFindTreeItem = (treeData, value) => {
    for (let item of treeData) {
        if (item[props.dataProps.value] === value) return item;

        if (item.children && item.children.length > 0) {
            let result = handleFindTreeItem(item.children, value);
            if (result) return result;
        }
    }
    return null;
}
// 下拉框选中时触发
const handleSelect = (config) => {
    data.value = config;
    selectRef.value.popoverConfig.data = data;
    if (props.multiple) {
        emit('update:modelValue', config.map(i => i[props.dataProps.value]));
        emit('change', config);
    } else {
        emit('update:modelValue', config[props.dataProps.value]);
        emit('change', config);
    }
}

// 多选框中删除其中某项
const handleCloseTag = (index) => {
    data.value.splice(index, 1);
    selectRef.value.popoverConfig.data = data.value;
    if (props.selectPopoverRef) {
        props.selectPopoverRef.handleInitData();
        props.selectPopoverRef.handleUpdatePopover();
    }
    emit('update:modelValue', data.value.map(i => i[props.dataProps.value]));
}

// 点击下拉框时传递ref
const handleTrigger = () => {
    if (props.disabled) return;
    if (filterInputRef.value) {
        filterInputRef.value.focus();
        filterInputIsFocus.value = true;
    }
    addItemsInputRef.value && addItemsInputRef.value.focus();
    selectRef.value.popoverConfig.data = data.value;
    selectRef.value.popoverConfig.options = props.options;
    emit('click', selectRef.value);
}

// filter输入框
const filterInputRef = ref(null);
const filterInputSpanRef = ref(null);
const filterInputValue = ref('');
const filterInputIsFocus = ref(false);
let filterInputTimer = null;
const handleFilterInput = () => {
    const maxWidth = selectRef.value.offsetWidth - 24 - 20;
    nextTick(() => filterInputRef.value.style.width = filterInputSpanRef.value.offsetWidth > maxWidth ? `${maxWidth}px` : `${filterInputSpanRef.value.offsetWidth || 1}px`);
    if (props.selectPopoverRef) {
        // 如果当前是关闭状态则手动触发开启下拉框
        !props.selectPopoverRef.getVisibleStatus() && emit('click', selectRef.value);
        filterInputTimer && clearTimeout(filterInputTimer);
        filterInputTimer = setTimeout(() => props.selectPopoverRef.handleSearch(filterInputValue.value), 0);
    }
}
const handleFilterInputBlur = () => {
    filterInputIsFocus.value = false;
    filterInputValue.value = '';
    props.selectPopoverRef && props.selectPopoverRef.handleUpdatePopover();
}

// addItems输入框
const addItemsInputRef = ref(null);
const addItemsInputSpanRef = ref(null);
const addItemsInputValue = ref('');
const handleAddItemsInput = () => {
    const maxWidth = selectRef.value.offsetWidth - 24 - 20;
    nextTick(() => addItemsInputRef.value.style.width = addItemsInputSpanRef.value.offsetWidth > maxWidth ? `${maxWidth}px` : `${addItemsInputSpanRef.value.offsetWidth || 1}px`);
}
const handleAddItemsInputBlur = () => {
    if (!addItemsInputValue.value) return;
    // 判断是否为options中的一项
    const hasItem = props.options.find(i => i[props.dataProps.label] === addItemsInputValue.value);
    // 获取原本选中的项
    let oldValue = data.value.map(i => i[props.dataProps.value]);
    if (hasItem) {
        // 替换掉输入值为options的key
        addItemsInputValue.value = hasItem[props.dataProps.value];
        // 去除inputValue中含有的
        oldValue = oldValue.filter(i => i !== hasItem[props.dataProps.value]);
    }
    emit('update:modelValue', [...oldValue, addItemsInputValue.value]);
    // 更新下拉框的选中项
    nextTick(() => props.selectPopoverRef && props.selectPopoverRef.handleUpdateChooseData(data.value));
    addItemsInputValue.value = '';
    addItemsInputRef.value.style.width = '1px';
}

// 清空
const handleClear = () => {
    data.value = props.multiple ? [] : '';
    selectRef.value.popoverConfig.data = data.value;
    props.selectPopoverRef && props.selectPopoverRef.handleInitData()
    emit('update:modelValue', data.value);
    emit('click');
}

// 移入移出
const isHovering = ref(false);
const handleHover = (e, enterFlag) => {
    isHovering.value = enterFlag;
}
</script>

<style lang="scss" scoped>
:deep(.el-tag__content) {
    max-width: 100px!important;
    overflow: hidden;
    text-overflow: ellipsis;
}
</style>

  1. index.vue 实例页面
  • 最基础的单选下拉框,包含搜索、清除等功能
<template>
    <div style="padding: 100px;background-color: #fff;">
        <SelectContainer v-model="value" :options="options" clearable :currentPopoverTarget="selectPopoverConfig.virtualRef" :selectPopoverRef="selectPopoverRef" @click="handleShowSelectPopover" style="width: 250px" />

        <SelectPopover ref="selectPopoverRef" :virtualRef="selectPopoverConfig.virtualRef" :visible="selectPopoverConfig.visible" @resetSelectPopoverConfig="handleResetSelectPopoverConfig" />
    </div>
</template>

<script setup>
import SelectContainer from './components/select.vue';
import SelectPopover from './components/popover.vue';
const value = ref('');
const options = ref([
    {
        value: '1',
        label: 'Option 1',
    },
    {
        value: '2',
        label: 'Option 2',
    },
    {
        value: '3',
        label: 'Option 3'
    }
]);

// 下拉框
const selectPopoverRef = ref(null);
const selectPopoverConfig = ref({
    virtualRef: {},
    visible: false
});
// 打开下拉框
const handleShowSelectPopover = (target) => {
    if (!target) return;
    if (selectPopoverConfig.value.virtualRef !== target) {
        handleResetSelectPopoverConfig();     // 先将之前的虚拟节点重置
        // 延迟设置新的虚拟节点,出现新的下拉动画
        setTimeout(() => {
            selectPopoverConfig.value.virtualRef = target;
            selectPopoverConfig.value.visible = true;
        }, 0);
        return;
    }
    selectPopoverConfig.value.visible = !selectPopoverConfig.value.visible;
    !selectPopoverConfig.value.visible && (selectPopoverConfig.value.virtualRef = {});
}
// 重置下拉框
const handleResetSelectPopoverConfig = () => {
    selectPopoverConfig.value.virtualRef = {};
    selectPopoverConfig.value.visible = false;
}
</script>

screen_recording_2025-06-26_17-16-04.gif

  • 多选下拉框,包含添加items、清除等功能,注意添加items和搜索功能不能同时使用
<template>
    <div style="padding: 100px;background-color: #fff;">
        <SelectContainer v-model="value" :options="options" clearable multiple addItems :currentPopoverTarget="selectPopoverConfig.virtualRef" :selectPopoverRef="selectPopoverRef" @click="handleShowSelectPopover" style="width: 250px" />
        
        <SelectPopover ref="selectPopoverRef" :virtualRef="selectPopoverConfig.virtualRef" :visible="selectPopoverConfig.visible" @resetSelectPopoverConfig="handleResetSelectPopoverConfig" />
    </div>
</template>
<script setup>
import SelectContainer from './components/select.vue';
import SelectPopover from './components/popover.vue';

const value = ref('');
const options = ref([
    {
        value: '1',
        label: 'Option 1',
    },
    {
        value: '2',
        label: 'Option 2',
    },
    {
        value: '3',
        label: 'Option 3',
    },
]);

// 下拉框
const selectPopoverRef = ref(null);
const selectPopoverConfig = ref({
    virtualRef: {},
    visible: false
});
// 打开下拉框
const handleShowSelectPopover = (target) => {
    if (!target) return;
    if (selectPopoverConfig.value.virtualRef !== target) {
        handleResetSelectPopoverConfig();     // 先将之前的虚拟节点重置
        // 延迟设置新的虚拟节点,出现新的下拉动画
        setTimeout(() => {
            selectPopoverConfig.value.virtualRef = target;
            selectPopoverConfig.value.visible = true;
        }, 0);
        return;
    }
    selectPopoverConfig.value.visible = !selectPopoverConfig.value.visible;
    !selectPopoverConfig.value.visible && (selectPopoverConfig.value.virtualRef = {});
}
// 重置下拉框
const handleResetSelectPopoverConfig = () => {
    selectPopoverConfig.value.virtualRef = {};
    selectPopoverConfig.value.visible = false;
}
</script>

screen_recording_2025-06-26_17-14-03.gif

  • 树形下拉框,包含搜索、清除等功能
<template>
    <div style="padding: 100px;background-color: #fff;">
        <SelectContainer v-model="value2" :options="options2" clearable filterable isTree :currentPopoverTarget="selectPopoverConfig.virtualRef" :selectPopoverRef="selectPopoverRef" @click="handleShowSelectPopover" style="width: 250px" />
        
        <SelectPopover ref="selectPopoverRef" :virtualRef="selectPopoverConfig.virtualRef" :visible="selectPopoverConfig.visible" @resetSelectPopoverConfig="handleResetSelectPopoverConfig" />
    </div>
</template>
<script setup>
import SelectContainer from './components/select.vue';
import SelectPopover from './components/popover.vue';
const value2 = ref('');
const options2 = ref([
    {
        value: '1',
        label: 'Option 1',
        children: [
            {
                value: '1-1',
                label: 'Option 1-1',
            },
            {
                value: '1-2',
                label: 'Option 1-2',
            },
        ]
    },
    {
        value: '2',
        label: 'Option 2',
        children: [
            {
                value: '2-1',
                label: 'Option 2-1',
            },
            {
                value: '2-2',
                label: 'Option 2-2',
            }
        ]
    },
    {
        value: '3',
        label: 'Option 3',
    },
]);

// 下拉框
const selectPopoverRef = ref(null);
const selectPopoverConfig = ref({
    virtualRef: {},
    visible: false
});
// 打开下拉框
const handleShowSelectPopover = (target) => {
    if (!target) return;
    if (selectPopoverConfig.value.virtualRef !== target) {
        handleResetSelectPopoverConfig();     // 先将之前的虚拟节点重置
        // 延迟设置新的虚拟节点,出现新的下拉动画
        setTimeout(() => {
            selectPopoverConfig.value.virtualRef = target;
            selectPopoverConfig.value.visible = true;
        }, 0);
        return;
    }
    selectPopoverConfig.value.visible = !selectPopoverConfig.value.visible;
    !selectPopoverConfig.value.visible && (selectPopoverConfig.value.virtualRef = {});
}
// 重置下拉框
const handleResetSelectPopoverConfig = () => {
    selectPopoverConfig.value.virtualRef = {};
    selectPopoverConfig.value.visible = false;
}
</script>

screen_recording_2025-06-26_17-20-05.gif

写到最后,还有好多功能没有展示,都在 select.vue 中备注着,可以根据需要自行添加,还可以自己DIY更多功能,比如加虚拟滚动组件实现大数据量的下拉选择等等,没有做不到,只有想不到✌️