起因是因为在工作中遇到了一个需求需要使用el-transfer,但是由于数据量接近有1w条,会导致该组件非常的卡顿;
看到的一篇文章中提到了两种解决方法,一种是分页,但是分页功能并不满足需求,所以只能使用虚拟滚动来实现需求,刚好本人在以前学习过虚拟滚动的实现方法。
所以本篇文章重点就不讲如何实现虚拟滚动了。
业务实现
首先需要将element-ui中的transfer拷贝至自己的components文件夹下,直接将整改文件夹复制过去即可
文件路径: node_modules\element-ui\packages\transfer
虚拟滚动实现代码:
transfer-panel.vue
template:
唯一修改的地方(使用的是viewData)
<el-checkbox-group
v-model="checked"
v-show="!hasNoMatch && data.length > 0"
:class="{ 'is-filterable': filterable }"
class="el-transfer-panel__list">
<div class="checkbox-container_background"></div>
<div class="checkbox-container">
<el-checkbox
class="el-transfer-panel__item"
:label="item[keyProp]"
:disabled="item[disabledProp]"
v-for="item in viewData"
:key="item[keyProp]">
<option-content :option="item"></option-content>
</el-checkbox>
</div>
</el-checkbox-group>
style
.el-transfer-panel {
width: 220px;
}
.el-transfer-panel__filter {
margin: 15px 30px;
}
.el-checkbox-group {
position: relative;
}
.checkbox-container_background {
position: absolute;
left: 0;
right: 0;
top: 0;
z-index: -1;
}
.checkbox-container {
position: absolute;
left: 0;
right: 0;
top: 0;
}
script:
props: {
className: String
},
data() {
return {
checked: [],
allChecked: false,
query: '',
inputHover: false,
checkChangeByUser: true,
viewData: [], // 试图渲染的数据
listData: [] // 全部数据
};
},
// 主要改造是在watch中的filteredData中
watch: {
filteredData(val, oldVal) {
// 第一次接收到数据的时候渲染视图
if (!oldVal.length || oldVal.length < 10 || val.length < 20) {
this.viewData = val.slice(0, val.length < 20 ? val.length : 20)
}
// 移动之后移动top刷新视图
this.movedRefreshView(val, oldVal)
// 讲移动之后的数据从viewData中过滤并把之后的元素填充
this.filterViewData()
// 变动之后的全部数据
this.listData = [...this.filteredData]
// 设置虚拟容器的高度
this.setVirtualBackHeight()
},
},
computed: {
filteredData() {
return this.data.filter(item => {
if (typeof this.filterMethod === 'function') {
return this.filterMethod(this.query, item);
} else {
const label = item[this.labelProp] || item[this.keyProp].toString();
return label.toLowerCase().indexOf(this.query.toLowerCase()) > -1;
}
}).map((item, index) => {
return {
...item,
top: 30 * index // 视图容器滚动的top标记
}
});
},
},
mounted() {
this.initVirtualScroll({
backEl: `.${this.className} .checkbox-container_background`,
childEl: `.${this.className} ` + '.el-transfer-panel__item',
contentEl: `.${this.className} ` + '.checkbox-container',
outEl: `.${this.className} ` + '.el-transfer-panel__list',
})
},
methods: {
// 相关方法注册
initVirtualScroll({ contentEl, outEl }) {
const MAX_COUNT = 10
let switchScrollScale = []
let ITEM_HEIGHT = 30
const contentDom = document.querySelector(contentEl)
const outDom = document.querySelector(outEl)
let tick = false // 节流开关
outDom.addEventListener('scroll', (e) => {
if (!tick) {
tick = true
window.requestAnimationFrame(() => {
tick = false
})
getRunDataList(e.target.scrollTop, { contentDom })
}
})
const getRunDataList = (distance, { contentDom }) => {
// 因为有beforeList和afterList的数据兜底
// 所以可以在scrollTop处于nowList的数据上的时候不渲染数据
// 等到滚动出了安全区的时候再渲染数据
if (!switchScroll(distance)) {
const startIndex = getStartIndex(distance)
this.startIndex = startIndex
const beforeList = this.listData.slice(getBeforeIndex(startIndex), startIndex)
const renderData = this.listData.slice(getBeforeIndex(startIndex), getAfterIndex(startIndex) + MAX_COUNT)
changeListTop(contentDom, startIndex, beforeList[0] || this.listData[startIndex])
// 改变安全区
changeSwitchScale(startIndex, getBeforeIndex(startIndex), getAfterIndex(startIndex))
// 修改试图区数据
this.changeViewData(renderData)
}
}
// 移动list元素到指定位置
function changeListTop (contentDom, startIndex, { top }) {
contentDom.style.transform = `translate3d(0, ${top}px, 0)`
}
// 判断是否在安全区
function switchScroll (scrollTop) {
return scrollTop > switchScrollScale[0] && scrollTop < switchScrollScale[1]
}
// 改变安全区
function changeSwitchScale (startIndex, beforeIndex, afterIndex) {
const beforeScale = Math.ceil(startIndex) * ITEM_HEIGHT
const afterScale = Math.floor((afterIndex)) * ITEM_HEIGHT
switchScrollScale = [beforeScale, afterScale]
}
// 二分法查找
const getStartIndex = (scrollTop) => {
let start = 0
let end = this.listData.length - 1
while (start < end) {
const mid = Math.floor((end + start) / 2)
const { top } = this.listData[mid]
if (scrollTop >= top && scrollTop < top + ITEM_HEIGHT) {
start = mid
break
} else if (scrollTop >= top + ITEM_HEIGHT) {
start = mid + 1
} else if (scrollTop < top) {
end = mid - 1
}
}
return start < 0 ? 0 : start
}
function getBeforeIndex (startIndex) {
return startIndex - MAX_COUNT < 0 ? 0 : startIndex - MAX_COUNT
}
const getAfterIndex = (startIndex) => {
return startIndex + MAX_COUNT > this.listData.length ? this.listData.length : startIndex + MAX_COUNT
}
},
setScrollTop(index) {
document.querySelector(`.${this.className} ` + '.el-transfer-panel__list').scrollTop = index * 30
},
changeViewData(data) {
this.viewData = [...data]
},
movedRefreshView(val, oldVal) {
const getViewIndex = () => {
const viewLength = this.viewData.length
let result = 0
if (viewLength <= 10) {
result = 0
} else if (viewLength <= 20) {
result = viewLength - 11
} else if (viewLength <= 30) {
result = 10
}
return val.length > oldVal.length ? 0 : result
}
// 初始化不改变top
if (val.length === oldVal.length) return
const viewIndex = getViewIndex()
const index = this.filteredData.findIndex((item) => item.id === this.viewData[viewIndex]?.id)
if (index !== -1) {
this.setScrollTop(index)
}
},
setVirtualBackHeight() {
document.querySelector(`.${this.className} .checkbox-container_background`).style.height
= this.listData.length * 30 + 'px'
},
filterViewData() {
const filteredDataKeys = this.filteredData.map(item => item[this.keyProp]);
this.viewData = this.viewData.filter((item) => filteredDataKeys.includes(item[this.keyProp]))
},
// 全选方法性能优化
updateAllChecked() {
let allKeyProps = {};
const checkAbleDataKeys = this.checkableData.map(item => {
let keyProps = {};
keyProps[item[this.keyProp]] = true;
allKeyProps[item[this.keyProp]] = true;
return keyProps;
});
this.allChecked =
checkAbleDataKeys.length > 0 &&
this.checked.length > 0 &&
this.checked.every((item) => allKeyProps[item]);
},
}
main.vue
在实现以上虚拟滚动之后,发现向右添加数据的时候还是会卡顿,需要优化mian.vue中的过滤算法
主要就是将原组件中嵌套循环的方法改成了单一循环(O(n²) ->O(n))
template
在两个transfer-panel组件上绑定className属性,并且给予对应类名
<div class="el-transfer">
<transfer-panel
v-loading="isTransLate"
v-bind="$props"
class="leftPanel"
:className="'leftPanel'"
ref="leftPanel"
:data="sourceData"
:title="titles[0] || t('el.transfer.titles.0')"
:default-checked="leftDefaultChecked"
:placeholder="filterPlaceholder || t('el.transfer.filterPlaceholder')"
@checked-change="onSourceCheckedChange">
<slot name="left-footer"></slot>
</transfer-panel>
<div class="el-transfer__buttons">
<el-button
type="primary"
:class="['el-transfer__button', hasButtonTexts ? 'is-with-texts' : '']"
@click.native="addToLeft"
:disabled="isTransLate || rightChecked.length === 0">
<i class="el-icon-arrow-left"></i>
<span v-if="buttonTexts[0] !== undefined">{{ buttonTexts[0] }}</span>
</el-button>
<el-button
type="primary"
:class="['el-transfer__button', hasButtonTexts ? 'is-with-texts' : '']"
@click.native="addToRight"
:disabled="isTransLate || leftChecked.length === 0">
<span v-if="buttonTexts[1] !== undefined">{{ buttonTexts[1] }}</span>
<i class="el-icon-arrow-right"></i>
</el-button>
</div>
<transfer-panel
v-loading="isTransLate"
v-bind="$props"
ref="rightPanel"
:className="'rightPanel'"
class="rightPanel"
:data="targetData"
:title="titles[1] || t('el.transfer.titles.1')"
:default-checked="rightDefaultChecked"
:placeholder="filterPlaceholder || t('el.transfer.filterPlaceholder')"
@checked-change="onTargetCheckedChange">
<slot name="right-footer"></slot>
</transfer-panel>
script
computed: {
dataObj() {
const key = this.props.key;
return this.data.reduce((o, cur) => (o[cur[key]] = cur) && o, {});
},
sourceData() {
const valueObj = {}
this.value.forEach(item => valueObj[item] = true)
return this.data.filter(item => !valueObj[item[this.props.key]]);
},
targetData() {
if (this.targetOrder === 'original') {
const valueObj = {}
this.value.forEach(item => valueObj[item] = true)
return this.data.filter(item => valueObj[item[this.props.key]]);
} else {
return this.value.reduce((arr, cur) => {
const val = this.dataObj[cur];
if (val) {
arr.push(val);
}
return arr;
}, []);
}
},
hasButtonTexts() {
return this.buttonTexts.length === 2;
}
},
methods: {
addToRight() {
let currentValue = this.value.slice();
const itemsToBeMoved = [];
const key = this.props.key;
let leftCheckedKeyPropsObj = {};
this.leftChecked.forEach((item) => {
leftCheckedKeyPropsObj[item] = true;
});
let valueKeyPropsObj = {};
this.value.forEach((item) => {
valueKeyPropsObj[item] = true;
});
this.data.forEach((item) => {
const itemKey = item[key];
if ( leftCheckedKeyPropsObj[itemKey] && !valueKeyPropsObj[itemKey] ) {
itemsToBeMoved.push(itemKey);
}
});
currentValue = this.targetOrder === 'unshift'
? itemsToBeMoved.concat(currentValue)
: currentValue.concat(itemsToBeMoved);
this.$emit('input', currentValue);
this.$emit('change', currentValue, 'right', this.leftChecked);
},
}
全部代码:
main.vue
<template>
<div class="el-transfer">
<transfer-panel
v-loading="isTransLate"
v-bind="$props"
class="leftPanel"
:className="'leftPanel'"
ref="leftPanel"
:data="sourceData"
:title="titles[0] || t('el.transfer.titles.0')"
:default-checked="leftDefaultChecked"
:placeholder="filterPlaceholder || t('el.transfer.filterPlaceholder')"
@checked-change="onSourceCheckedChange">
<slot name="left-footer"></slot>
</transfer-panel>
<div class="el-transfer__buttons">
<el-button
type="primary"
:class="['el-transfer__button', hasButtonTexts ? 'is-with-texts' : '']"
@click.native="addToLeft"
:disabled="isTransLate || rightChecked.length === 0">
<i class="el-icon-arrow-left"></i>
<span v-if="buttonTexts[0] !== undefined">{{ buttonTexts[0] }}</span>
</el-button>
<el-button
type="primary"
:class="['el-transfer__button', hasButtonTexts ? 'is-with-texts' : '']"
@click.native="addToRight"
:disabled="isTransLate || leftChecked.length === 0">
<span v-if="buttonTexts[1] !== undefined">{{ buttonTexts[1] }}</span>
<i class="el-icon-arrow-right"></i>
</el-button>
</div>
<transfer-panel
v-loading="isTransLate"
v-bind="$props"
ref="rightPanel"
:className="'rightPanel'"
class="rightPanel"
:data="targetData"
:title="titles[1] || t('el.transfer.titles.1')"
:default-checked="rightDefaultChecked"
:placeholder="filterPlaceholder || t('el.transfer.filterPlaceholder')"
@checked-change="onTargetCheckedChange">
<slot name="right-footer"></slot>
</transfer-panel>
</div>
</template>
<script>
import ElButton from 'element-ui/packages/button';
import Emitter from 'element-ui/src/mixins/emitter';
import Locale from 'element-ui/src/mixins/locale';
import TransferPanel from './transfer-panel.vue';
import Migrating from 'element-ui/src/mixins/migrating';
export default {
name: 'ElTransfer',
mixins: [Emitter, Locale, Migrating],
components: {
TransferPanel,
ElButton
},
props: {
data: {
type: Array,
default() {
return [];
}
},
titles: {
type: Array,
default() {
return [];
}
},
buttonTexts: {
type: Array,
default() {
return [];
}
},
filterPlaceholder: {
type: String,
default: ''
},
filterMethod: Function,
leftDefaultChecked: {
type: Array,
default() {
return [];
}
},
rightDefaultChecked: {
type: Array,
default() {
return [];
}
},
renderContent: Function,
value: {
type: Array,
default() {
return [];
}
},
format: {
type: Object,
default() {
return {};
}
},
filterable: Boolean,
props: {
type: Object,
default() {
return {
label: 'label',
key: 'key',
disabled: 'disabled'
};
}
},
targetOrder: {
type: String,
default: 'original'
}
},
data() {
return {
leftChecked: [],
rightChecked: [],
isTransLate: false
};
},
computed: {
dataObj() {
const key = this.props.key;
return this.data.reduce((o, cur) => (o[cur[key]] = cur) && o, {});
},
sourceData() {
const valueObj = {}
this.value.forEach(item => valueObj[item] = true)
return this.data.filter(item => !valueObj[item[this.props.key]]);
},
targetData() {
if (this.targetOrder === 'original') {
const valueObj = {}
this.value.forEach(item => valueObj[item] = true)
return this.data.filter(item => valueObj[item[this.props.key]]);
} else {
return this.value.reduce((arr, cur) => {
const val = this.dataObj[cur];
if (val) {
arr.push(val);
}
return arr;
}, []);
}
},
hasButtonTexts() {
return this.buttonTexts.length === 2;
}
},
watch: {
value(val) {
this.dispatch('ElFormItem', 'el.form.change', val);
}
},
methods: {
getMigratingConfig() {
return {
props: {
'footer-format': 'footer-format is renamed to format.'
}
};
},
onSourceCheckedChange(val, movedKeys) {
this.leftChecked = val;
if (movedKeys === undefined) return;
this.$emit('left-check-change', val, movedKeys);
},
onTargetCheckedChange(val, movedKeys) {
this.rightChecked = val;
if (movedKeys === undefined) return;
this.$emit('right-check-change', val, movedKeys);
},
addToLeft() {
let currentValue = this.value.slice();
this.rightChecked.forEach(item => {
const index = currentValue.indexOf(item);
if (index > -1) {
currentValue.splice(index, 1);
}
});
this.$emit('input', currentValue);
this.$emit('change', currentValue, 'left', this.rightChecked);
},
addToRight() {
let currentValue = this.value.slice();
const itemsToBeMoved = [];
const key = this.props.key;
let leftCheckedKeyPropsObj = {};
this.leftChecked.forEach((item) => {
leftCheckedKeyPropsObj[item] = true;
});
let valueKeyPropsObj = {};
this.value.forEach((item) => {
valueKeyPropsObj[item] = true;
});
this.data.forEach((item) => {
const itemKey = item[key];
if ( leftCheckedKeyPropsObj[itemKey] && !valueKeyPropsObj[itemKey] ) {
itemsToBeMoved.push(itemKey);
}
});
currentValue = this.targetOrder === 'unshift'
? itemsToBeMoved.concat(currentValue)
: currentValue.concat(itemsToBeMoved);
this.$emit('input', currentValue);
this.$emit('change', currentValue, 'right', this.leftChecked);
},
clearQuery(which) {
if (which === 'left') {
this.$refs.leftPanel.query = '';
} else if (which === 'right') {
this.$refs.rightPanel.query = '';
}
}
}
};
</script>
transfer-panel.vue
<template>
<div class="el-transfer-panel">
<p class="el-transfer-panel__header">
<el-checkbox
v-model="allChecked"
@change="handleAllCheckedChange"
:indeterminate="isIndeterminate">
{{ title }}
<span>{{ checkedSummary }}</span>
</el-checkbox>
</p>
<div :class="['el-transfer-panel__body', hasFooter ? 'is-with-footer' : '']">
<el-input
class="el-transfer-panel__filter"
v-model="query"
size="small"
:placeholder="placeholder"
@mouseenter.native="inputHover = true"
@mouseleave.native="inputHover = false"
v-if="filterable">
<i slot="prefix"
:class="['el-input__icon', 'el-icon-' + inputIcon]"
@click="clearQuery"
></i>
</el-input>
<el-checkbox-group
v-model="checked"
v-show="!hasNoMatch && data.length > 0"
:class="{ 'is-filterable': filterable }"
class="el-transfer-panel__list">
<div class="checkbox-container_background"></div>
<div class="checkbox-container">
<el-checkbox
class="el-transfer-panel__item"
:label="item[keyProp]"
:disabled="item[disabledProp]"
v-for="item in viewData"
:key="item[keyProp]">
<option-content :option="item"></option-content>
</el-checkbox>
</div>
</el-checkbox-group>
<p
class="el-transfer-panel__empty"
v-show="hasNoMatch">{{ t('el.transfer.noMatch') }}</p>
<p
class="el-transfer-panel__empty"
v-show="data.length === 0 && !hasNoMatch">{{ t('el.transfer.noData') }}</p>
</div>
<p class="el-transfer-panel__footer" v-if="hasFooter">
<slot></slot>
</p>
</div>
</template>
<script>
import ElCheckboxGroup from 'element-ui/packages/checkbox-group';
import ElCheckbox from 'element-ui/packages/checkbox';
import ElInput from 'element-ui/packages/input';
import Locale from 'element-ui/src/mixins/locale';
export default {
mixins: [Locale],
name: 'ElTransferPanel',
componentName: 'ElTransferPanel',
components: {
ElCheckboxGroup,
ElCheckbox,
ElInput,
OptionContent: {
props: {
option: Object
},
render(h) {
const getParent = vm => {
if (vm.$options.componentName === 'ElTransferPanel') {
return vm;
} else if (vm.$parent) {
return getParent(vm.$parent);
} else {
return vm;
}
};
const panel = getParent(this);
const transfer = panel.$parent || panel;
return panel.renderContent
? panel.renderContent(h, this.option)
: transfer.$scopedSlots.default
? transfer.$scopedSlots.default({ option: this.option })
: <span>{ this.option[panel.labelProp] || this.option[panel.keyProp] }</span>;
}
}
},
props: {
data: {
type: Array,
default() {
return [];
}
},
renderContent: Function,
placeholder: String,
title: String,
filterable: Boolean,
format: Object,
filterMethod: Function,
defaultChecked: Array,
props: Object,
className: String
},
data() {
return {
checked: [],
allChecked: false,
query: '',
inputHover: false,
checkChangeByUser: true,
viewData: [],
listData: []
};
},
watch: {
checked(val, oldVal) {
this.updateAllChecked();
if (this.checkChangeByUser) {
const movedKeys = val.concat(oldVal)
.filter(v => val.indexOf(v) === -1 || oldVal.indexOf(v) === -1);
this.$emit('checked-change', val, movedKeys);
} else {
this.$emit('checked-change', val);
this.checkChangeByUser = true;
}
},
filteredData(val, oldVal) {
if (!oldVal.length || oldVal.length < 10 || val.length < 20) {
this.viewData = val.slice(0, val.length < 20 ? val.length : 20)
}
this.movedRefreshView(val, oldVal)
this.filterViewData()
this.listData = [...this.filteredData]
this.setVirtualBackHeight()
},
data() {
const checked = [];
const filteredDataKeys = this.filteredData.map(item => item[this.keyProp]);
this.checked.forEach(item => {
if (filteredDataKeys.indexOf(item) > -1) {
checked.push(item);
}
});
this.checkChangeByUser = false;
this.checked = checked;
},
checkableData() {
this.updateAllChecked();
},
defaultChecked: {
immediate: true,
handler(val, oldVal) {
if (oldVal && val.length === oldVal.length &&
val.every(item => oldVal.indexOf(item) > -1)) return;
const checked = [];
const checkableDataKeys = this.checkableData.map(item => item[this.keyProp]);
val.forEach(item => {
if (checkableDataKeys.indexOf(item) > -1) {
checked.push(item);
}
});
this.checkChangeByUser = false;
this.checked = checked;
}
}
},
computed: {
filteredData() {
return this.data.filter(item => {
if (typeof this.filterMethod === 'function') {
return this.filterMethod(this.query, item);
} else {
const label = item[this.labelProp] || item[this.keyProp].toString();
return label.toLowerCase().indexOf(this.query.toLowerCase()) > -1;
}
}).map((item, index) => {
return {
...item,
top: 30 * index
}
});
},
checkableData() {
return this.filteredData.filter(item => !item[this.disabledProp]);
},
checkedSummary() {
const checkedLength = this.checked.length;
const dataLength = this.data.length;
const { noChecked, hasChecked } = this.format;
if (noChecked && hasChecked) {
return checkedLength > 0
? hasChecked.replace(/${checked}/g, checkedLength).replace(/${total}/g, dataLength)
: noChecked.replace(/${total}/g, dataLength);
} else {
return `${ checkedLength }/${ dataLength }`;
}
},
isIndeterminate() {
const checkedLength = this.checked.length;
return checkedLength > 0 && checkedLength < this.checkableData.length;
},
hasNoMatch() {
return this.query.length > 0 && this.filteredData.length === 0;
},
inputIcon() {
return this.query.length > 0 && this.inputHover
? 'circle-close'
: 'search';
},
labelProp() {
return this.props.label || 'label';
},
keyProp() {
return this.props.key || 'key';
},
disabledProp() {
return this.props.disabled || 'disabled';
},
hasFooter() {
return !!this.$slots.default;
}
},
mounted() {
this.$nextTick(() => {
this.initVirtualScroll({
backEl: `.${this.className} .checkbox-container_background`,
childEl: `.${this.className} ` + '.el-transfer-panel__item',
contentEl: `.${this.className} ` + '.checkbox-container',
outEl: `.${this.className} ` + '.el-transfer-panel__list',
})
})
},
methods: {
initVirtualScroll({ contentEl, outEl }) {
const MAX_COUNT = 10
let switchScrollScale = []
let ITEM_HEIGHT = 30
const contentDom = document.querySelector(contentEl)
const outDom = document.querySelector(outEl)
let tick = false // 节流开关
outDom.addEventListener('scroll', (e) => {
if (!tick) {
tick = true
window.requestAnimationFrame(() => {
tick = false
})
getRunDataList(e.target.scrollTop, { contentDom })
}
})
const getRunDataList = (distance, { contentDom }) => {
// 因为有beforeList和afterList的数据兜底
// 所以可以在scrollTop处于nowList的数据上的时候不渲染数据
// 等到滚动出了安全区的时候再渲染数据
if (!switchScroll(distance)) {
const startIndex = getStartIndex(distance)
this.startIndex = startIndex
const beforeList = this.listData.slice(getBeforeIndex(startIndex), startIndex)
// const nowList = this.listData.slice(startIndex, startIndex + MAX_COUNT)
// const afterList
// = this.listData.slice(getAfterIndex(startIndex), getAfterIndex(startIndex) + MAX_COUNT)
const renderData = this.listData.slice(getBeforeIndex(startIndex), getAfterIndex(startIndex) + MAX_COUNT)
changeListTop(contentDom, startIndex, beforeList[0] || this.listData[startIndex])
// 改变安全区
changeSwitchScale(startIndex, getBeforeIndex(startIndex), getAfterIndex(startIndex))
// 修改试图区数据
this.changeViewData(renderData)
}
}
// 移动list元素到指定位置
function changeListTop (contentDom, startIndex, { top }) {
contentDom.style.transform = `translate3d(0, ${top}px, 0)`
}
// 判断是否在安全区
function switchScroll (scrollTop) {
return scrollTop > switchScrollScale[0] && scrollTop < switchScrollScale[1]
}
// 改变安全区
function changeSwitchScale (startIndex, beforeIndex, afterIndex) {
const beforeScale = Math.ceil(startIndex) * ITEM_HEIGHT
const afterScale = Math.floor((afterIndex)) * ITEM_HEIGHT
switchScrollScale = [beforeScale, afterScale]
}
// 二分法查找
const getStartIndex = (scrollTop) => {
let start = 0
let end = this.listData.length - 1
while (start < end) {
const mid = Math.floor((end + start) / 2)
const { top } = this.listData[mid]
if (scrollTop >= top && scrollTop < top + ITEM_HEIGHT) {
start = mid
break
} else if (scrollTop >= top + ITEM_HEIGHT) {
start = mid + 1
} else if (scrollTop < top) {
end = mid - 1
}
}
return start < 0 ? 0 : start
}
function getBeforeIndex (startIndex) {
return startIndex - MAX_COUNT < 0 ? 0 : startIndex - MAX_COUNT
}
const getAfterIndex = (startIndex) => {
return startIndex + MAX_COUNT > this.listData.length ? this.listData.length : startIndex + MAX_COUNT
}
},
setScrollTop(index) {
document.querySelector(`.${this.className} ` + '.el-transfer-panel__list').scrollTop = index * 30
},
changeViewData(data) {
this.viewData = [...data]
},
movedRefreshView(val, oldVal) {
const getViewIndex = () => {
const viewLength = this.viewData.length
let result = 0
if (viewLength <= 10) {
result = 0
} else if (viewLength <= 20) {
result = viewLength - 11
} else if (viewLength <= 30) {
result = 10
}
return val.length > oldVal.length ? 0 : result
}
const viewIndex = getViewIndex()
const index = this.filteredData.findIndex((item) => item.id === this.viewData[viewIndex]?.id)
if (index !== -1) {
this.setScrollTop(index)
}
},
setVirtualBackHeight() {
document.querySelector(`.${this.className} .checkbox-container_background`).style.height
= this.listData.length * 30 + 'px'
},
filterViewData() {
const filteredDataKeys = this.filteredData.map(item => item[this.keyProp]);
this.viewData = this.viewData.filter((item) => filteredDataKeys.includes(item[this.keyProp]))
},
updateAllChecked() {
let allKeyProps = {};
const checkAbleDataKeys = this.checkableData.map(item => {
let keyProps = {};
keyProps[item[this.keyProp]] = true;
allKeyProps[item[this.keyProp]] = true;
return keyProps;
});
this.allChecked =
checkAbleDataKeys.length > 0 &&
this.checked.length > 0 &&
this.checked.every((item) => allKeyProps[item]);
},
handleAllCheckedChange(value) {
this.checked = value
? this.checkableData.map(item => item[this.keyProp])
: [];
},
clearQuery() {
if (this.inputIcon === 'circle-close') {
this.query = '';
}
}
}
};
</script>
<style scope>
.el-transfer-panel {
width: 220px;
}
.el-transfer-panel__filter {
margin: 15px 30px;
}
.el-checkbox-group {
position: relative;
}
.checkbox-container_background {
position: absolute;
left: 0;
right: 0;
top: 0;
z-index: -1;
}
.checkbox-container {
position: absolute;
left: 0;
right: 0;
top: 0;
}
</style>
优化想法以及思路来自大佬的文章(www.cnhackhy.com/84221.htm