ElementUI中el-Select 选择器自定义底部固定

文章介绍了如何在Vue应用中使用el-select组件时,实现在选择框底部动态添加自定义端口选项的功能,包括监听change事件、操作popper元素以及自定义端口按钮的点击行为。

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

​
<el-select
   v-model="form.scanPort"
   placeholder="请选择扫描端口"
   @change="getScanPort"
   popper-append-to-body
   ref="refSelect" 
   @visible-change="VisibleChange"
  >
   <el-option
    v-for="item in scanPortList"
    :key="item.id"
    :label="item.name"
    :value="item.id"
    />
</el-select>

methods:{

VisibleChange() {
      let bkpRef = this.$refs.refSelect;
      let popper = bkpRef.$refs.popper;
      if (popper.$el) popper = popper.$el
      if (!Array.from(popper.children).some(v => v.className === 'bkp_add_fixed_bottom_select')) {
        const el = document.createElement('div');
        el.className = 'bkp_add_fixed_bottom_select';
        el.style = `
                       font-size: 14px;
                       font-weight: 500;
                       height: 42px;
                       background: #0691b1;
                       color: #fff;
                       display: flex;
                       align-items: center;
                       justify-content: center;
                       cursor: pointer;
                       border-top:1px solid rgb(240 242 245)
                 `
        el.innerHTML = `
                  <el-option label="" value="">
                  <div slot @click="customPort"
                  <i class="el-icon-plus"></i> 自定义端口组</div></el-option>`;

        popper.appendChild(el);
        el.onclick = () => {
          this.customPort();
          // 以下代码实现点击后弹层隐藏 不需要可以删掉
          if (bkpRef.toggleDropDownVisible) {
            bkpRef.toggleDropDownVisible(false);
          }
          else {
            bkpRef.visible = false;
          }
        };
      }
    },
}

<think>我们需要创建一个Vue3组件,使用Element PlusSelect组件,并实现以下功能: 1. 支持远程搜索(通过接口搜索) 2. 支持懒加载(滚动到底部时加载更多) 3. 远程搜索时输入防抖 组件设计思路: - 使用`el-select`和`el-option`组件 - 通过`remote`和`filterable`开启远程搜索 - 使用`remote-method`定义远程搜索方法,并在其中加入防抖 - 使用`v-infinite-scroll`指令实现懒加载(需要将选项放在一个可滚动的容器中,并监听滚动事件) 注意:Element PlusSelect组件本身不支持懒加载,我们需要结合`el-select`和`el-option`以及一个自定义的滚动容器来实现。 步骤: 1. 创建组件,设置`el-select`,绑定`v-model`。 2. 设置`filterable`和`remote`属性,使用`remote-method`绑定一个防抖的搜索方法。 3. 使用一个自定义的容器(比如`div`)包裹`el-option`,并设置固定高度和滚动,然后使用`v-infinite-scroll`指令来监听滚动到底部的事件,触发加载更多。 4. 管理状态:当前页码、搜索关键词、是否正在加载、是否没有更多数据等。 由于`el-select`内部会使用虚拟滚动,但这里我们需要自己控制懒加载,所以不使用虚拟滚动,而是使用一个固定高度的容器来放置选项,并监听滚动事件。 但是,Element PlusSelect组件默认不支持在选项列表上直接添加`v-infinite-scroll`,所以我们可以通过自定义下拉内容(使用`slot`)来实现。 具体实现: - 使用`el-select`的`slot`自定义下拉内容,使用`el-option`来渲染每个选项,同时将选项列表放在一个具有固定高度的`div`中,并在这个`div`上使用`v-infinite-scroll`。 防抖实现:使用lodash的`debounce`或者自己写一个防抖函数。 代码结构: 模板部分: <el-select v-model="selectedValue" filterable remote :remote-method="remoteMethod" :loading="loading" placeholder="请选择" > <div v-infinite-scroll="loadMore" infinite-scroll-disabled="noMore || loadingMore" infinite-scroll-distance="50" style="max-height: 200px; overflow-y: auto;"> <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" /> </div> </el-select> 但是,这样写可能会遇到问题,因为Element PlusSelect组件默认的下拉框是虚拟滚动的,我们自定义下拉内容后需要自己控制滚动。 另一种思路:不使用自定义下拉内容,而是监听滚动事件。但是,由于el-select内部结构,我们无法直接获取到选项列表的DOM。 因此,我们采用自定义下拉内容的方式,并自己管理选项的展示和滚动。 但是,注意:自定义下拉内容后,需要自己处理选项的点击选择事件,因为el-option自定义下拉内容中已经与el-select内部逻辑解耦。所以,我们需要手动处理选择事件。 所以,我们改为使用`el-option`的点击事件,并手动设置选中值。 完整步骤: 1. 自定义下拉内容,使用`el-option`并绑定点击事件,在点击时更新`selectedValue`并关闭下拉框。 2.自定义下拉内容中,使用一个固定高度的容器包裹选项列表,并添加`v-infinite-scroll`指令。 3. 在远程搜索方法中,重置分页,并调用接口获取第一页数据。 4. 在滚动到底部时,加载下一页数据。 注意:在远程搜索时,需要重置分页参数,并且清空当前选项列表。 代码实现: 由于Element PlusSelect组件在自定义下拉内容时,需要手动设置`slot`,并且需要处理很多细节,这里我们采用以下方式: 模板: <el-select v-model="selectedValue" filterable remote :remote-method="handleSearch" :loading="loading" placeholder="请选择" @focus="handleFocus" // 当聚焦时,可以加载初始数据(如果需要) > <template #default> <div ref="scrollContainer" class="infinite-select" v-infinite-scroll="loadMore" :infinite-scroll-disabled="noMore || loadingMore" :infinite-scroll-distance="50"> <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" @click="handleOptionClick(item.value)" /> </div> </template> </el-select> 但是,这样写可能无法正确显示下拉框的宽度和样式,我们可以使用`popper-class`给下拉框添加一个类,然后设置样式。 另外,注意:自定义下拉内容后,原生的`el-option`点击事件不会触发select的选中,所以我们需要手动处理。 我们使用`@click`事件在`el-option`上,当点击时,设置`selectedValue`为当前选项的值,并手动关闭下拉框(通过改变一个状态?)。但是,el-select并没有直接提供关闭下拉框的方法。 我们可以通过给`el-select`添加`ref`,然后调用`blur`方法使其失去焦点来关闭下拉框。但是这样会触发失去焦点事件,可能不是最佳方式。 另一种方式是使用`visible-change`事件来控制下拉框的显示,但这样会增加复杂度。 实际上,Element PlusSelect组件在点击选项后会自动关闭下拉框,但是当我们自定义下拉内容后,这个行为失效了。所以我们需要手动触发关闭。 我们可以利用`el-select`的`blur`方法,但是这样会使得整个select失去焦点,可能会影响用户体验。 或者,我们可以使用`el-select`的`collapseTags`方法?没有这个方法。 官方文档中,自定义选项模板的示例并没有处理点击事件,而是直接使用`el-option`,这样应该是可以自动处理点击事件的。但是,我们这里自定义了整个下拉内容,所以`el-option`被包裹在自定义容器中,可能无法被el-select内部识别。 因此,我们尝试不自定义整个下拉内容,而是只自定义选项列表,但仍然使用`el-option`,并利用`el-select`的默认行为。但是,我们无法在`el-option`的容器上直接添加无限滚动,因为el-select默认使用虚拟滚动。 考虑到这些复杂性,我们可以换一种方式:不使用自定义下拉内容,而是通过监听滚动事件。但是,我们如何获取到el-select下拉框内部的滚动容器呢? 在Element Plus中,Select组件下拉框的滚动容器是`.el-select-dropdown__wrap`,我们可以在`mounted`后尝试获取这个元素,然后添加滚动事件监听。但是,由于下拉框是动态生成的,并且可能多次打开关闭,所以我们需要在每次下拉框打开时重新获取并监听。 具体步骤: 1. 使用`el-select`,设置`filterable`、`remote`、`remote-method`(防抖的搜索方法)等属性。 2. 在`mounted`中,使用`MutationObserver`或者在下拉框显示时(通过`visible-change`事件)去获取滚动容器,然后添加滚动事件监听。 3. 在滚动事件中,判断是否滚动到底部,然后触发加载更多。 这样避免了自定义下拉内容的复杂性。 我们选择第二种方式:通过监听原生滚动事件。 实现: 模板: <el-select ref="selectRef" v-model="selectedValue" filterable remote :remote-method="handleSearch" :loading="loading" placeholder="请选择" :popper-append-to-body="false" // 为了更方便获取popper元素 @visible-change="handleVisibleChange" > <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" /> </el-select> 然后,在`handleVisibleChange`方法中,当下拉框显示时,我们获取下拉框的滚动容器,并添加滚动事件监听。当下拉框隐藏时,移除监听。 但是,注意:每次下拉框显示时,滚动容器可能会重新创建,所以每次都需要重新获取。 另外,由于是懒加载,我们需要记录当前页码、关键词等。 考虑到时间,我们采用第二种方案。 下面是一个完整的实现代码示例(使用Vue3 Composition API):</think>### Vue3 + Element Plus 实现懒加载/远程搜索 Select 选择器(含防抖) 以下是一个完整的可复用组件实现,支持懒加载滚动加载和远程搜索防抖功能: ```vue <template> <el-select v-model="selectedValue" filterable remote :remote-method="handleSearch" :loading="loading" placeholder="请选择或搜索" @visible-change="handleVisibleChange" @focus="handleFocus" > <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" /> <div v-if="loadingMore" class="loading-more"> <el-icon class="is-loading"><Loading /></el-icon> <span>加载中...</span> </div> <div v-if="noMoreData" class="no-more">没有更多数据了</div> </el-select> </template> <script setup> import { ref, onMounted, onUnmounted } from 'vue'; import { ElSelect, ElOption, ElIcon } from 'element-plus'; import { Loading } from '@element-plus/icons-vue'; const props = defineProps({ // 初始加载数据的API函数 fetchData: { type: Function, required: true }, // 搜索API函数 searchData: { type: Function, required: true }, // 防抖时间(毫秒) debounceTime: { type: Number, default: 500 } }); const selectedValue = ref(''); const options = ref([]); const loading = ref(false); const loadingMore = ref(false); const noMoreData = ref(false); const currentPage = ref(1); const pageSize = ref(10); const searchQuery = ref(''); const isSearching = ref(false); const scrollContainer = ref(null); let debounceTimer = null; // 初始加载数据 const loadInitialData = async () => { try { loading.value = true; const response = await props.fetchData({ page: currentPage.value, pageSize: pageSize.value }); options.value = response.data; noMoreData.value = response.total <= pageSize.value; attachScrollListener(); } catch (error) { console.error('数据加载失败:', error); } finally { loading.value = false; } }; // 处理远程搜索(带防抖) const handleSearch = (query) => { searchQuery.value = query; isSearching.value = !!query; // 清除之前的防抖定时器 if (debounceTimer) clearTimeout(debounceTimer); // 设置新的防抖定时器 debounceTimer = setTimeout(async () => { try { loading.value = true; options.value = []; if (query) { const response = await props.searchData(query); options.value = response.data; noMoreData.value = true; // 搜索时不加载更多 } else { // 清空搜索时恢复初始数据 currentPage.value = 1; await loadInitialData(); } } catch (error) { console.error('搜索失败:', error); } finally { loading.value = false; } }, props.debounceTime); }; // 滚动加载更多数据 const loadMoreData = async () => { if (loadingMore.value || noMoreData.value || isSearching.value) return; try { loadingMore.value = true; currentPage.value += 1; const response = await props.fetchData({ page: currentPage.value, pageSize: pageSize.value }); options.value = [...options.value, ...response.data]; noMoreData.value = (currentPage.value * pageSize.value) >= response.total; } catch (error) { console.error('加载更多失败:', error); currentPage.value -= 1; // 失败时回退页码 } finally { loadingMore.value = false; } }; // 监听滚动事件 const handleScroll = () => { if (!scrollContainer.value) return; const { scrollTop, clientHeight, scrollHeight } = scrollContainer.value; const distanceToBottom = scrollHeight - scrollTop - clientHeight; // 当滚动到底部50px内时加载更多 if (distanceToBottom < 50) { loadMoreData(); } }; // 附加滚动监听器 const attachScrollListener = () => { nextTick(() => { const dropdown = document.querySelector('.el-select-dropdown .el-scrollbar__wrap'); if (dropdown) { scrollContainer.value = dropdown; dropdown.addEventListener('scroll', handleScroll); } }); }; // 移除滚动监听器 const removeScrollListener = () => { if (scrollContainer.value) { scrollContainer.value.removeEventListener('scroll', handleScroll); scrollContainer.value = null; } }; // 下拉框显示状态变化 const handleVisibleChange = (visible) => { if (visible) { attachScrollListener(); } else { removeScrollListener(); } }; // 聚焦时加载初始数据 const handleFocus = () => { if (options.value.length === 0 && !isSearching.value) { loadInitialData(); } }; onMounted(() => { // 初始加载数据 loadInitialData(); }); onUnmounted(() => { removeScrollListener(); if (debounceTimer) clearTimeout(debounceTimer); }); </script> <style scoped> .loading-more, .no-more { padding: 8px 0; text-align: center; color: var(--el-text-color-secondary); font-size: 12px; } .loading-more .el-icon { margin-right: 5px; animation: rotating 2s linear infinite; } @keyframes rotating { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } </style> ``` ### 使用示例 ```vue <template> <div> <h3>用户选择器</h3> <LazySearchSelect :fetch-data="fetchUsers" :search-data="searchUsers" debounce-time="300" /> </div> </template> <script setup> import LazySearchSelect from './LazySearchSelect.vue'; // 模拟API请求 const fetchUsers = async ({ page, pageSize }) => { // 实际项目中替换为真实API调用 console.log(`加载用户数据: 第${page}页, 每页${pageSize}条`); // 模拟网络延迟 await new Promise(resolve => setTimeout(resolve, 800)); // 模拟返回数据 const startIndex = (page - 1) * pageSize; const data = Array.from({ length: pageSize }, (_, i) => ({ value: startIndex + i + 1, label: `用户 ${startIndex + i + 1}` })); return { data, total: 100 // 模拟总数据量 }; }; // 模拟搜索API const searchUsers = async (query) => { console.log(`搜索用户: ${query}`); // 模拟网络延迟 await new Promise(resolve => setTimeout(resolve, 500)); // 模拟返回搜索结果 return { data: Array.from({ length: 5 }, (_, i) => ({ value: `search_${i}`, label: `搜索结果 ${i+1}: ${query}` })), total: 5 }; }; </script> ``` ### 功能特点 1. **懒加载功能** - 滚动到底部自动加载下一页数据 - 显示加载状态和没有更多数据的提示 - 智能分页管理 2. **远程搜索功能** - 输入时自动触发搜索 - 防抖处理(默认500ms,可配置) - 清空搜索框时恢复初始数据 3. **性能优化** - 滚动事件节流处理 - 组件卸载时清理事件监听器 - 避免重复请求 4. **UI/UX增强** - 加载状态视觉反馈 - 平滑滚动体验 - 响应式设计 ### 使用说明 1. 通过 `fetch-data` 属性传入数据加载函数 2. 通过 `search-data` 属性传入搜索函数 3. 通过 `debounce-time` 设置防抖时间(默认500ms) 4. 组件会自动处理分页、滚动加载和搜索逻辑
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值