场景:
项目需要在表格中,插入input、select下拉列表等元素。当数据量过大时,会导致浏览器渲染超负荷,甚至内存过大直接崩溃,而此时的select下拉框还没加载渲染完成。
插件:
结合个人项目使用场景,输出如下代码:
用法:
<virtual-scroll-select
:list="dataIdLists"
name="dataid"
key-name="id"
:dataValues.sync="scope.row.dataid"
@change="changeDataId(scope.$index, 'dataid', scope.row.dataid, scope.row)">
</virtual-scroll-select>
实现:
HTML
<template>
<div class="virtual-select">
<el-popover
ref="popover"
placement="bottom"
popper-class="virtual-select-popover"
:visible-arrow="false"
@show="onShow"
@hide="onHide"
v-model="isVisible"
trigger="focus">
<!-- manual(手动开启关闭,配合v-model) focus -->
<div class="scroll-container" :style="{'--scrollHeight': scrollHeight}">
<RecycleScroller
ref="list"
class="scroller"
:key="pageModeFullPage"
:items="filteredOptions"
:item-size="null"
:page-mode="false"
size-field="optionHeight"
:key-field="keyName"
itemClass="option-box"
>
<template #default="{ item }">
<div
class="option"
:class="{'active-option': item[keyName] === selectedItem}"
@click="selectItem(item)"
>
<span>{{ item[keyName] }}</span>
</div>
</template>
<template #empty v-if="filteredOptions.length === 0">
<div class="scroller-empty">
暂无数据
</div>
</template>
</RecycleScroller>
</div>
</el-popover>
<v-input
v-popover:popover
v-model="searchText"
placeholder="请选择"
@input="onSearch">
</v-input>
</div>
</template>
JS
<script>
import { RecycleScroller } from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
import VInput from '@/components/VInput'
import _ from 'lodash'
export default {
components: {
RecycleScroller,
VInput
},
props: {
list: {
type: Array,
required: true
},
dataValues: {
type: String,
default: ''
},
keyName: {
type: String,
default: ''
}
},
data () {
return {
searchText: '',
filteredOptions: [],
isDropdownVisible: true,
selectedItem: '',
pageMode: true,
pageModeFullPage: true,
isVisible: false
}
},
computed: {
scrollHeight () {
// 根据数据量调整下拉popover显示高度
// return '200px'
return this.filteredOptions.length > 6
? '200px'
: (this.filteredOptions.length === 0
? '40px'
: this.filteredOptions.length * 32 + 'px'
)
}
},
watch: {
list: {
handler: function (nv, ov) {
// console.log('hd', nv)
this.filteredOptions = nv
},
deep: true
},
dataValues: function (nv) {
this.selectedItem = nv
this.searchText = nv
}
},
methods: {
onShow () {
// this.btnClick()
const vm = this
this.$nextTick(() => {
const i = vm.list.findIndex(item => {
return item[vm.keyName] === vm.dataValues
})
console.log(vm.dataValues)
console.log(i)
/**
* 用于打开popover时,回显,滚动到当前已选择数据位置;
插件内置scrollToItem()方法
*/
vm.$refs.list.scrollToItem(i)
})
},
onSearch: _.debounce(function (newValue) {
// console.log(newValue)
this.$emit('update:dataValues', newValue)
this.filteredOptions = this.list.filter(option =>
option[this.keyName].toLowerCase().includes(this.searchText.toLowerCase())
)
// console.log(this.filteredOptions)
}, 300),
selectItem (item) {
console.log('Selected:', item)
this.selectedItem = item[this.keyName]
this.searchText = item[this.keyName]
this.isVisible = false
this.$emit('update:dataValues', item[this.keyName])
this.$emit('change', item)
// 这里可以添加逻辑来处理选中项,比如关闭下拉列表、更新外部状态等
},
onHide () {
if (this.searchText !== this.selectedItem) {
// 当用户输入搜索过,但未选择任意数据,则恢复至上次绑定;
this.searchText = this.selectedItem
this.$emit('update:dataValues', this.selectedItem)
}
this.filteredOptions = this.list
}
},
mounted () {
this.selectedItem = this.dataValues
this.searchText = this.dataValues
this.filteredOptions = this.list
}
}
</script>
<style lang="less" scoped>
.virtual-select {
width: 100%;
position: relative;
}
.scroll-container {
// overflow-y: auto;
height: var(--scrollHeight);
// max-height: 200px;
background: #fefefe;
.scroller {
width: 100%;
height: 100%;
::v-deep .option-box {
border-bottom: 1px solid #e9eaf0;
font-size: 14px;
cursor: pointer;
height: 32px;
line-height: 19px;
padding: 6px 12px;
box-sizing: border-box;
.option {
color: #616163;
text-align: center;
background: #fff;
box-sizing: border-box;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
box-sizing: border-box;
border-radius: 4px;
&:hover {
color: #fff;
background: #6d71f9;
// .option {
// color: #616163;
// }
}
span {
display: inline-block;
width: 100%;
height: 100%;
padding: 0 10px;
box-sizing: border-box;
}
}
.active-option {
color: #fff;
background: #6d71f9;
}
}
.scroller-empty {
padding: 10px 0;
margin: 0;
text-align: center;
color: #999;
font-size: 14px;
}
/** 滚动条样式调整 */
&::-webkit-scrollbar {
width: 6px;
height: 6px;
transition: background-color .2s linear, width .2s ease-in-out;
}
&::-webkit-scrollbar-thumb {
border-radius: 10px;
background: rgba(0,0,0,0.1);
}
&::-webkit-scrollbar-thumb:hover {
width: 10px;
background: rgba(0,0,0,0.3);
}
&::-webkit-scrollbar-track {
border-radius: 10px;
background: rgba(0,0,0,0.2);
}
&::-webkit-scrollbar-thumb:active {
width: 10px;
background: rgba(0,0,0,.6);
}
}
}
.fade-enter-active, .fade-leave-active {
transition: opacity .5s;
}
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
opacity: 0;
}
</style>
<style lang="less">
.virtual-select-popover {
margin: 0;
border-color: #e9eaf0;
border-radius: 0;
box-shadow: 0px 0px 10px 0px rgba(168, 174, 187, 0.25);
border: 1px solid #E4E7ED;
padding: 0;
box-sizing: border-box;
}
</style>