vue3中element-plus组件库有虚拟滚动select,然而维护的是vue2项目,遇到后端要返回9000条数据的情况,需要进行下拉选择并且可以过滤
<template>
<div class="e-stat__select" :style="{ width: width, minWidth: minWidth }">
<div class="e-select-main">
<div
class="e-select--mask"
v-if="showSelector"
@click="showSelector = false"
/>
<div
class="e-select"
:class="{
'e-select-disabled': disabled,
isfocus: focus || showSelector,
}"
>
<div class="e-select__input-box" @click.stop="openSelectList">
<input
class="e-select__input-text"
v-model="currentData"
:placeholder="placeholder"
@input="filter"
@focus="focus = true"
@blur="focus = false"
v-if="search && !disabled"
/>
<div class="e-select__input-text" v-else>
{{ currentData || currentData === 0 ? currentData : placeholder }}
div>
<div class="e-select-icon" @click.stop="toggleSelector" v-else>
<i
class="el-icon-arrow-up arrowAnimation"
:class="showSelector ? 'top' : 'bottom'"
/>
div>
div>
<transition name="el-zoom-in-top">
<div class="e-select__selector" v-if="showSelector">
<div class="e-popper__arrow">div>
<div
class="e-select__selector-scroll"
@scroll="scroll"
ref="scroll"
>
<div class="parentDom">
<div :style="{ height: screenHeight + 'px' }">div>
<div
class="positionRelative"
:style="{ transform: getTransform }"
>
<div
class="e-select__selector-empty"
v-if="currentOptions.length === 0"
>
<div>{{ emptyTips }}div>
div>
<div
v-else
class="e-select__selector-item"
:class="[
{ highlight: currentData == item[props.text] },
{
'e-select__selector-item-disabled':
item[props.disabled],
},
]"
v-for="(item, index) in visibleData"
:key="index"
@click.stop="change(item)"
>
<div>{{ item[props.text] }}div>
div>
div>
div>
div>
div>
transition>
div>
div>
div>
template>
<script>
export default {
name: "e-vir-select",
data() {
return {
// 当前值
currentData: "",
// 当前选项列表
currentOptions: [],
/** 是否显示下拉选择列表 */
showSelector: false,
/** 偏移高度 */
startOffset: 0,
/** 起始显示数据 */
start: 0,
/** 结束显示数据 */
end: 10,
/** 预留的dom,避免快速滚动空白 */
remain: 40,
focus: false,
};
},
props: {
// 选项列表
options: {
type: Array,
default() {
return [];
},
},
// 选项列表数据格式
props: {
type: Object,
default() {
return {
value: "value",
text: "text",
disabled: "disabled",
};
},
},
// vue2 v-model传值方式
value: {
type: [String, Number],
default: "",
},
// vue3 v-model传值方式
modelValue: {
type: [String, Number],
default: "",
},
// 占位
placeholder: {
type: String,
default: "请选择",
},
// 宽度
width: {
type: String,
default: "500px",
},
// 最小宽度
minWidth: {
type: String,
default: "120px",
},
// 空值占位
emptyTips: {
type: String,
default: "暂无选项",
},
// 是否可清除
clear: {
type: Boolean,
default: false,
},
// 是否整体禁用
disabled: {
type: Boolean,
default: false,
},
// 每条数据的高度,注意注意,只支持px,修改每条数据的css高度后,才需要改变这个值
itemSize: {
type: Number,
default: 34,
},
// 启动搜索模式
search: {
type: Boolean,
default: true,
},
},
watch: {
options: {
handler() {
this.currentOptions = this.options;
this.initData();
},
deep: true,
immediate: true,
},
modelValue: {
handler() {
this.initData();
},
immediate: true,
},
value: {
handler() {
this.initData();
},
immediate: true,
},
},
computed: {
/** 根据每条数据的高度获取总列表高度,最低一个元素 */
screenHeight() {
return Math.max(
this.itemSize,
this.currentOptions.length * this.itemSize
);
},
/** 前面预留 */
prevCount() {
return Math.min(this.start, this.remain);
},
/** 后面预留 */
nextCount() {
return Math.min(this.remain, this.end);
},
/** 每次截取虚拟列表的位置 */
getTransform() {
return `translate(0,${this.startOffset}px)`;
},
/** 虚拟数据 */
visibleData() {
return this.currentOptions.slice(
this.start,
Math.min(this.end, this.currentOptions.length)
);
},
},
methods: {
/** 处理数据,此函数用于兼容vue2 vue3 */
initData() {
this.currentData = "";
// vue2
if (this.value || this.value === 0) {
for (let item of this.options) {
if (item[this.props.value] === this.value) {
this.currentData = item[this.props.text];
this.$emit("getText", this.currentData);
return;
}
}
}
// vue3
if (this.modelValue || this.modelValue === 0) {
for (let item of this.options) {
if (item[this.props.value] === this.modelValue) {
this.currentData = item[this.props.text];
this.$emit("getText", this.currentData);
return;
}
}
}
},
/** 过滤选项列表 */
filter() {
if (this.currentData) {
this.currentOptions = this.options.filter((item) => {
return item[this.props.text].indexOf(this.currentData) > -1;
});
} else {
this.currentOptions = this.options;
this.$emit("change", "清空");
this.emit("");
}
this.$refs.scroll.scrollTop = 0;
},
/** 选择选项 */
change(item) {
const { disabled, value } = this.props;
if (item[disabled]) return;
this.$emit("change", item);
this.emit(item[value]);
this.currentData = item[this.props.text];
this.currentOptions = this.options;
this.showSelector = false;
},
/** 还原值,清空值 */
clearVal() {
this.showSelector = false;
this.start = 0;
this.end = 5;
this.startOffset = 0;
this.$emit("change", "清空");
this.emit("");
},
/** 兼容vue2、vue3的v-model传值 */
emit(value) {
this.$emit("input", value);
this.$emit("update:modelValue", value);
},
/** 打开选择列表 */
openSelectList() {
if (this.disabled || this.showSelector) return;
this.showSelector = true;
// 找到当前值所在的索引
if (this.currentData) {
this.currentOptions = this.options;
for (let i = 0; i < this.options.length; i++) {
if (this.options[i][this.props.text] === this.currentData) {
this.start = i;
break;
}
}
this.end = this.start + this.nextCount + this.remain; // 此时的结束索引
this.startOffset = this.start * this.itemSize; // 此时的偏移量
this.$nextTick(() => {
this.$refs.scroll.scrollTop = this.start * this.itemSize; // 设置滚动
});
} else {
this.currentOptions = this.options;
this.scrollFn(0);
this.$nextTick(() => {
this.$refs.scroll.scrollTop = 0; // 设置滚动
});
}
},
/** 切换选择列表显示, */
toggleSelector() {
if (this.disabled) return;
this.showSelector = !this.showSelector;
this.currentOptions = this.options;
},
/** 滚动事件 */
scroll(e) {
this.scrollFn(e.target.scrollTop);
},
/** 滚动函数 */
scrollFn(scrollTop) {
// 此时的开始索引
this.start =
Math.floor(scrollTop / this.itemSize) - this.prevCount - this.remain >=
0
? Math.floor(scrollTop / this.itemSize) - this.prevCount - this.remain
: 0;
this.end = this.start + this.nextCount + this.remain * 2; // 此时的结束索引
this.startOffset = this.start * this.itemSize; // 此时的偏移量
},
},
};
script>
<style lang="scss" scoped>
/deep/ ::-webkit-scrollbar {
width: 7px;
height: 7px;
}
/deep/ ::-webkit-scrollbar-thumb {
border-radius: 10px;
min-height: 70px;
background-color: #dedfe1;
}
.e-stat__select {
display: flex;
align-items: center;
cursor: pointer;
box-sizing: border-box;
min-width: 100%;
margin-right: 15px;
.e-select-main {
width: 100%;
}
.e-select-disabled {
background-color: #f5f7fa;
cursor: not-allowed;
}
.e-select--mask {
position: fixed;
top: 0;
bottom: 0;
right: 0;
left: 0;
}
.e-select {
font-size: 14px;
box-sizing: border-box;
border-radius: 4px;
padding: 0 5px;
position: relative;
display: flex;
user-select: none;
flex-direction: row;
align-items: center;
border: 1px solid #dcdfe6;
border-bottom: solid 1px #dddddd;
.e-select__input-box {
width: 100%;
height: 34px;
position: relative;
display: flex;
flex: 1;
flex-direction: row;
align-items: center;
input {
width: 100%;
height: 100%;
border: none;
outline: none;
}
.e-select-icon {
width: 50px;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.arrowAnimation {
color: #c0c4cc;
transition: transform 0.3s;
}
.top {
transform: rotateZ(0deg);
}
.bottom {
transform: rotateZ(180deg);
}
.e-select__input-text {
padding-left: 7px;
width: 100%;
color: #333;
font-size: 16px;
white-space: nowrap;
text-overflow: ellipsis;
-o-text-overflow: ellipsis;
overflow: hidden;
}
}
.e-select__selector {
box-sizing: border-box;
position: absolute;
top: calc(100% + 12px);
left: 0;
width: 100%;
background-color: #ffffff;
border: 1px solid #ebeef5;
border-radius: 6px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
z-index: 999;
padding: 4px 0;
.e-popper__arrow,
.e-popper__arrow::after {
position: absolute;
display: block;
width: 0;
height: 0;
left: 50%;
border-color: transparent;
border-style: solid;
border-width: 6px;
}
.e-popper__arrow {
filter: drop-shadow(0 2px 12px rgba(0, 0, 0, 0.03));
top: -6px;
left: 50%;
transform: translateX(-50%);
margin-right: 3px;
border-top-width: 0;
border-bottom-color: #ebeef5;
}
.e-popper__arrow::after {
content: " ";
top: 1px;
margin-left: -6px;
border-top-width: 0;
border-bottom-color: #fff;
}
.e-select__selector-scroll {
overflow: auto;
max-height: 200px;
.parentDom {
position: relative;
.positionRelative {
width: 100%;
position: absolute;
left: 0;
top: 0;
font-size: 16px;
}
}
.e-select__selector-empty,
.e-select__selector-item {
display: flex;
cursor: pointer;
/* prettier-ignore*/
height: 34PX;
/* prettier-ignore*/
line-height: 34PX;
font-size: 14px;
text-align: center;
padding: 0px 10px;
box-sizing: border-box;
white-space: nowrap;
}
.e-select__selector-item:hover {
background-color: #f9f9f9;
}
.e-select__selector-empty:last-child,
.e-select__selector-item:last-child {
border-bottom: none;
}
.e-select__selector-item-disabled {
color: #b1b1b1;
cursor: not-allowed;
}
.highlight {
color: #409eff;
font-weight: bold;
}
}
}
}
.isfocus {
border: 1px solid #2f91f5;
}
}
style>