(Transfer)解决:Element-ui 中 Transfer 穿梭框因数据量过大而渲染卡顿问题的三种方法

本文主要是介绍(Transfer)解决:Element-ui 中 Transfer 穿梭框因数据量过大而渲染卡顿问题的三种方法,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

解决 Transfer 穿梭框卡顿问题:

  • Ⅰ、Element-ui 提供的 Transfer 组件信息:
    • 1、Transfer 组件代码:
    • 2、运行页面截图:
  • Ⅱ、存在的问题:
    • 1、页面卡顿的描述:
  • Ⅲ、解决卡顿的方案一:
    • 1、采用定时器的方法:
    • 2、代码为:
    • 3、页面展示为:
  • Ⅳ、解决卡顿的方案二(懒加载):
    • 1、需要将 element-ui 的 transfer 的组件拉出来进行二次封装:
    • 2、代码为:
      • 其一、App.vue 的代码:
      • 其二、transfer-panel.vue 的代码:
      • 其三、main.vue 的代码:
    • 3、页面展示为:
  • Ⅴ、解决卡顿的方案三(虚拟滚动):
    • 1、可能存在问题的描述:
    • 2、创建新的 transfer-checkbox-item.vue 文件及其它文件的修改:
    • 3、代码为:
      • 其一、transfer-checkbox-item.vue 的代码:
      • 其二、transfer-panel.vue 的代码:
      • 其三、main.vue 的代码:
      • 其四、App.vue 的代码:
    • 4、页面展示为:
  • Ⅵ、小结:

Ⅰ、Element-ui 提供的 Transfer 组件信息:

1、Transfer 组件代码:

在这里插入图片描述


// Element-ui 提供的组件代码:
<template><el-transfer v-model="value" :data="data"></el-transfer>
</template><script>export default {data() {const generateData = _ => {const data = [];for (let i = 1; i <= 15; i++) {data.push({key: i,label: `备选项 ${ i }`,disabled: i % 4 === 0});}return data;};return {data: generateData(),value: [1, 4]};}};
</script>

代码地址:https://element.eleme.cn/#/zh-CN/component/transfer

2、运行页面截图:

在这里插入图片描述

Ⅱ、存在的问题:

1、页面卡顿的描述:

无论是切换左侧菜单栏来加载该 Transfer 组件页面,还是刷新该页面加载该 Transfer 组件页面,都会出现因数据量过大(即:4094),而导致的页面渲染卡顿的问题(即:卡顿时间约 3s 左右);

Ⅲ、解决卡顿的方案一:

1、采用定时器的方法:

虽然不能完全解决该问题,但可以实现先展示,后加载的效果;
即:页面可以先展示出部分信息,等待定时器结束后,再将剩余的数据信息展示出来;

2、代码为:


// 注意:此时采用的是 vue3 的语法,若是 vue2 修改一下即可;
<template><div class="greetings" id="app"><el-transfer v-model="value" :data="data" style="display: block;" /></div>
</template>
<script setup lang="ts">
import { ref } from 'vue'const generateData = () => {const data = []for (let i = 1; i <= 500; i++) {data.push({key: i,label: `Option ${i}`,disabled: i % 4 === 0,})}// 此时添加 setTimeout 函数,是为了解决页面因数据量过大而卡顿 3s+ 的问题;setTimeout(addData,10)return data
}const addData = () => {for (let i = 501; i <= 4094; i++) {data.value.push({key: i,label: `VLAN ${i}`,disabled: i === 1})}
}const data = ref(generateData())
const value = ref([])
</script>

3、页面展示为:

// 此时项目的页面展示会有两个页面,相差 1-2s (即:两次渲染,500,4094);
在这里插入图片描述

在这里插入图片描述

Ⅳ、解决卡顿的方案二(懒加载):

1、需要将 element-ui 的 transfer 的组件拉出来进行二次封装:

其一、将 transfer 的组件拉出来:
在这里插入图片描述
其二、编辑 transfer-panel.vue 文件:

// 添加 count 属性:
在这里插入图片描述

// 添加 load() 方法:
在这里插入图片描述

// 修改后的 el-checkbox-group 为:
// 即:只渲染 0-count 条数数据,并且添加 v-infinite-scroll 和 infinite-scroll-distance 属性值;
在这里插入图片描述

// 此时页面就能正常展示及实现懒加载;

在这里插入图片描述

// 虽然此时的懒加载 4094 个数据没问题,但是在更大的数据量下的点击全选、点击任意一个 checkbox、或点击移动按钮,
// 仍然会卡顿很久,因此需要继续优化 transfer 组件;

// 优化全选的 updateAllChecked 方法:
在这里插入图片描述

// 优化单选某个checkbox节点时的逻辑:
在这里插入图片描述

其三、编辑 main.vue 文件:

// 优化移动逻辑(即:addToRight() 方法):

在这里插入图片描述

// 优化两个 computed 中的方法:

// 优化 sourceData() 函数;
在这里插入图片描述

// 优化 targetData() 函数;

在这里插入图片描述

// 此时通过懒加载,优化大量数据的代码就结束了;

// 此时的数据近两万条,完全没有问题;
在这里插入图片描述

2、代码为:

其一、App.vue 的代码:


<template><div class="greetings" id="app"><customTransfer v-model="value" :data="data" style="display: block;" /></div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import customTransfer from './transfer/index.js'const generateData = () => {const data = []for (let i = 1; i <= 18999; i++) {data.push({key: i,label: `Option ${i}`,disabled: i % 4 === 0,})}return data
}const data = ref(generateData())
const value = ref([])
</script>

其二、transfer-panel.vue 的代码:


<template><div class="el-transfer-panel"><p class="el-transfer-panel__header"><el-checkboxv-model="allChecked"@change="handleAllCheckedChange":indeterminate="isIndeterminate">{{ title }}<span>{{ checkedSummary }}</span></el-checkbox></p><div :class="['el-transfer-panel__body', hasFooter ? 'is-with-footer' : '']"><el-inputclass="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-groupv-infinite-scroll="load" :infinite-scroll-distance="10"v-model="checked"v-show="!hasNoMatch && data.length > 0":class="{ 'is-filterable': filterable }"class="el-transfer-panel__list"><el-checkboxclass="el-transfer-panel__item":label="item[keyProp]":disabled="item[disabledProp]":key="item[keyProp]"v-for="item in filteredData.slice(0,count)"><option-content :option="item"></option-content></el-checkbox></el-checkbox-group><!-- <el-checkbox-groupv-infinite-scroll="load" :infinite-scroll-distance="10"v-model="checked"v-show="!hasNoMatch && data.length > 0":class="{ 'is-filterable': filterable }"class="el-transfer-panel__list"><el-checkboxclass="el-transfer-panel__item":label="item[keyProp]":disabled="item[disabledProp]":key="item[keyProp]"v-for="item in filteredData"><option-content :option="item"></option-content></el-checkbox></el-checkbox-group> --><pclass="el-transfer-panel__empty"v-show="hasNoMatch">{{ t('el.transfer.noMatch') }}</p><pclass="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},data() {return {checked: [],allChecked: false,query: '',inputHover: false,checkChangeByUser: true,// 无限滚动用,初始只渲染50条count:50};},watch: {// 此时是 transfer 组件提供的函数值 checked1();checked1(val, oldVal) {this.updateAllChecked();if (this.checkChangeByUser) {// O(n^2)的时间复杂度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;}},checked(val, oldVal) {this.updateAllChecked();let newObj = {};val.every((item)=>{newObj[item] = true;});let oldObj = {};oldVal.every((item)=>{oldObj[item] = true;});if (this.checkChangeByUser) {// O(n)const movedKeys = val.concat(oldVal).filter(v => newObj[v] || oldVal[v]);this.$emit('checked-change', val, movedKeys);} else {this.$emit('checked-change', val);this.checkChangeByUser = true;}},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;}});},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;}},methods: {load () {// 当用户滚动到列表的底部时,额外渲染多50条this.count += 50},// 此时是 transfer 组件提供的函数值 updateAllChecked();updateAllChecked1() {const checkableDataKeys = this.checkableData.map(item => item[this.keyProp]);// 这里是O(n^2)的时间复杂度this.allChecked = checkableDataKeys.length > 0 &&checkableDataKeys.every(item => this.checked.indexOf(item) > -1);},updateAllChecked() {let checkObj = {};this.checked.forEach((item) => {checkObj[item] = true;});this.allChecked =this.checkableData.length > 0 &&this.checked.length > 0 &&this.checkableData.every((item) => checkObj[item[this.keyProp]]);},handleAllCheckedChange(value) {this.checked = value? this.checkableData.map(item => item[this.keyProp]): [];},clearQuery() {if (this.inputIcon === 'circle-close') {this.query = '';}},}};
</script>

其三、main.vue 的代码:


<template><div class="el-transfer"><transfer-panelv-bind="$props"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-buttontype="primary":class="['el-transfer__button', hasButtonTexts ? 'is-with-texts' : '']"@click.native="addToLeft":disabled="rightChecked.length === 0"><i class="el-icon-arrow-left"></i><span v-if="buttonTexts[0] !== undefined">{{ buttonTexts[0] }}</span></el-button><el-buttontype="primary":class="['el-transfer__button', hasButtonTexts ? 'is-with-texts' : '']"@click.native="addToRight":disabled="leftChecked.length === 0"><span v-if="buttonTexts[1] !== undefined">{{ buttonTexts[1] }}</span><i class="el-icon-arrow-right"></i></el-button></div><transfer-panelv-bind="$props"ref="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: []};},computed: {dataObj() {const key = this.props.key;return this.data.reduce((o, cur) => (o[cur[key]] = cur) && o, {});},// 此时是 transfer 组件提供的函数值 sourceData();sourceData1() {return this.data.filter(item => this.value.indexOf(item[this.props.key]) === -1);},sourceData() {let valueObj = {};this.value.forEach((item)=>{valueObj[item] = true;});return this.data.filter((item) => !valueObj[item[this.props.key]]);},// 此时是 transfer 组件提供的函数值 targetData();targetData1() {if (this.targetOrder === 'original') {return this.data.filter(item => this.value.indexOf(item[this.props.key]) > -1);} else {return this.value.reduce((arr, cur) => {const val = this.dataObj[cur];if (val) {arr.push(val);}return arr;}, []);}},targetData() {if (this.targetOrder === 'original') {let valueObj = {};this.value.forEach((item)=>{valueObj[item] = true;});let data = this.data.filter((item) => valueObj[item[this.props.key]]);return data;} 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);},// 此时是 transfer 组件提供的函数值 addToRight();addToRight1() {let currentValue = this.value.slice();const itemsToBeMoved = [];const key = this.props.key;this.data.forEach(item => {const itemKey = item[key];if (this.leftChecked.indexOf(itemKey) > -1 &&this.value.indexOf(itemKey) === -1) {itemsToBeMoved.push(itemKey);}});currentValue = this.targetOrder === 'unshift'? itemsToBeMoved.concat(currentValue): currentValue.concat(itemsToBeMoved);this.$emit('input', currentValue);this.$emit('change', currentValue, 'right', this.leftChecked);},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];// O(n)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>

3、页面展示为:

在这里插入图片描述

Ⅴ、解决卡顿的方案三(虚拟滚动):

1、可能存在问题的描述:

懒加载的方式的缺点就是,当用户一直往下滚的话,一开始只渲染50条,然后随着用户一直往下滚的话,就会渲染100、150…200…1000,列表是真的会渲染出上千条,最终也会卡顿;
而虚拟滚动就能完美解决这问题,永远只渲染50条;
因此可以采用虚拟滚动的方式来解决卡顿的问题;

2、创建新的 transfer-checkbox-item.vue 文件及其它文件的修改:

其一、transfer-checkbox-item.vue 文件的代码为:


<template><el-checkboxclass="el-transfer-panel__item":label="source[keyProp]":disabled="source[disabledProp]"><option-content :option="source"></option-content>
</el-checkbox>
</template><script>import ElCheckbox from 'element-ui/packages/checkbox';export default {name: 'transfer-checkbox-item',props: {index: { // index of current itemtype: Number},source: { // here is: {uid: 'unique_1', text: 'abc'}type: Object,default() {return {};}},keyProp: {type: String},disabledProp: {type: String}},components: {ElCheckbox,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>;}}}};
</script>

其二、编辑 transfer-panel.vue 文件:

// 优化全选的 updateAllChecked 方法:

在这里插入图片描述

// 优化单选某个checkbox节点时的逻辑:

在这里插入图片描述
注意要执行一下安装命令: npm i vue-virtual-scroll-list

// 然后再在 transfer-panel.vue 文件中添加代码如下:

在这里插入图片描述
在这里插入图片描述
// 添加两个变量:
在这里插入图片描述

// 添加 virtualScroll() 方法:
在这里插入图片描述

// 修改 keyProp() 方法:

在这里插入图片描述
// 修改 disabledProp() 方法:
在这里插入图片描述

// 修改 checkbox 的渲染为:

在这里插入图片描述

其三、编辑 main.vue 文件:

// 优化移动逻辑(即:addToRight() 方法):

在这里插入图片描述

// 优化两个 computed 中的方法:

// 优化 sourceData() 函数;
在这里插入图片描述

// 优化 targetData() 函数;

在这里插入图片描述

// 在 prop 中接受一个 virtualScroll 属性:

在这里插入图片描述
其四、编辑 App.vue 文件:

// 注意:在调用 newTransfer 组件时,要传 :virtual-scroll="true" 属性值;

在这里插入图片描述

3、代码为:

其一、transfer-checkbox-item.vue 的代码:


<template><el-checkboxclass="el-transfer-panel__item":label="source[keyProp]":disabled="source[disabledProp]"><option-content :option="source"></option-content>
</el-checkbox>
</template><script>import ElCheckbox from 'element-ui/packages/checkbox';export default {name: 'transfer-checkbox-item',props: {index: { // index of current itemtype: Number},source: { // here is: {uid: 'unique_1', text: 'abc'}type: Object,default() {return {};}},keyProp: {type: String},disabledProp: {type: String}},components: {ElCheckbox,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>;}}}};
</script>

其二、transfer-panel.vue 的代码:


<template><div class="el-transfer-panel"><p class="el-transfer-panel__header"><el-checkboxv-model="allChecked"@change="handleAllCheckedChange":indeterminate="isIndeterminate">{{ title }}<span>{{ checkedSummary }}</span></el-checkbox></p><div :class="['el-transfer-panel__body', hasFooter ? 'is-with-footer' : '']"><el-inputclass="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-groupv-model="checked"v-show="!hasNoMatch && data.length > 0":class="{ 'is-filterable': filterable }"class="el-transfer-panel__list"><el-checkboxclass="el-transfer-panel__item":label="item[keyProp]":disabled="item[disabledProp]":key="item[keyProp]"v-for="item in filteredData"><option-content :option="item"></option-content></el-checkbox></el-checkbox-group> --><el-checkbox-groupv-model="checked"v-show="!hasNoMatch && data.length > 0":class="{ 'is-filterable': filterable }"class="el-transfer-panel__list"><virtual-list v-if="virtualScroll"style="height:100%;overflow-y: auto;":data-key="keyProp":data-sources="filteredData":data-component="itemComponent":extra-props="virtualListProps"/><template v-else><el-checkboxclass="el-transfer-panel__item":label="item[keyProp]":disabled="item[disabledProp]":key="item[keyProp]"v-for="item in filteredData"><option-content :option="item"></option-content></el-checkbox></template></el-checkbox-group><pclass="el-transfer-panel__empty"v-show="hasNoMatch">{{ t('el.transfer.noMatch') }}</p><pclass="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';import Item from './transfer-checkbox-item.vue';import VirtualList from 'vue-virtual-scroll-list';export default {mixins: [Locale],name: 'ElTransferPanel',componentName: 'ElTransferPanel',components: {ElCheckboxGroup,// 注册VirtualList'virtual-list': VirtualList,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},data() {return {checked: [],allChecked: false,query: '',inputHover: false,checkChangeByUser: true,itemComponent: Item,virtualListProps: {}};},watch: {checked1(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;}},checked(val, oldVal) {this.updateAllChecked();let newObj = {};val.every((item)=>{newObj[item] = true;});let oldObj = {};oldVal.every((item)=>{oldObj[item] = true;});if (this.checkChangeByUser) {// O(n)const movedKeys = val.concat(oldVal).filter(v => newObj[v] || oldVal[v]);this.$emit('checked-change', val, movedKeys);} else {this.$emit('checked-change', val);this.checkChangeByUser = true;}},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;}});},virtualScroll() {return this.$parent.virtualScroll;},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';},keyProp1() {return this.props.key || 'key';},keyProp() {this.virtualListProps.keyProp = this.props.key || 'key';return this.props.key || 'key';},disabledProp1() {return this.props.disabled || 'disabled';},disabledProp() {this.virtualListProps.disabledProp = this.props.disabled || 'disabled';return this.props.disabled || 'disabled';},hasFooter() {return !!this.$slots.default;}},methods: {updateAllChecked1() {const checkableDataKeys = this.checkableData.map(item => item[this.keyProp]);this.allChecked = checkableDataKeys.length > 0 &&checkableDataKeys.every(item => this.checked.indexOf(item) > -1);},updateAllChecked() {let checkObj = {};this.checked.forEach((item) => {checkObj[item] = true;});this.allChecked =this.checkableData.length > 0 &&this.checked.length > 0 &&this.checkableData.every((item) => checkObj[item[this.keyProp]]);},handleAllCheckedChange(value) {this.checked = value? this.checkableData.map(item => item[this.keyProp]): [];},clearQuery() {if (this.inputIcon === 'circle-close') {this.query = '';}}}};
</script>

其三、main.vue 的代码:


<template><div class="el-transfer"><transfer-panelv-bind="$props"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-buttontype="primary":class="['el-transfer__button', hasButtonTexts ? 'is-with-texts' : '']"@click.native="addToLeft":disabled="rightChecked.length === 0"><i class="el-icon-arrow-left"></i><span v-if="buttonTexts[0] !== undefined">{{ buttonTexts[0] }}</span></el-button><el-buttontype="primary":class="['el-transfer__button', hasButtonTexts ? 'is-with-texts' : '']"@click.native="addToRight":disabled="leftChecked.length === 0"><span v-if="buttonTexts[1] !== undefined">{{ buttonTexts[1] }}</span><i class="el-icon-arrow-right"></i></el-button></div><transfer-panelv-bind="$props"ref="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 [];}},virtualScroll: {type: Boolean,default: false},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: []};},computed: {dataObj() {const key = this.props.key;return this.data.reduce((o, cur) => (o[cur[key]] = cur) && o, {});},sourceData1() {return this.data.filter(item => this.value.indexOf(item[this.props.key]) === -1);},sourceData() {let valueObj = {};this.value.forEach((item)=>{valueObj[item] = true;});return this.data.filter((item) => !valueObj[item[this.props.key]]);},targetData1() {if (this.targetOrder === 'original') {return this.data.filter(item => this.value.indexOf(item[this.props.key]) > -1);} else {return this.value.reduce((arr, cur) => {const val = this.dataObj[cur];if (val) {arr.push(val);}return arr;}, []);}},targetData() {if (this.targetOrder === 'original') {let valueObj = {};this.value.forEach((item)=>{valueObj[item] = true;});let data = this.data.filter((item) => valueObj[item[this.props.key]]);return data;} 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);},addToRight1() {let currentValue = this.value.slice();const itemsToBeMoved = [];const key = this.props.key;this.data.forEach(item => {const itemKey = item[key];if (this.leftChecked.indexOf(itemKey) > -1 &&this.value.indexOf(itemKey) === -1) {itemsToBeMoved.push(itemKey);}});currentValue = this.targetOrder === 'unshift'? itemsToBeMoved.concat(currentValue): currentValue.concat(itemsToBeMoved);this.$emit('input', currentValue);this.$emit('change', currentValue, 'right', this.leftChecked);},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];// O(n)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>

其四、App.vue 的代码:


<template><div class="greetings" id="app"><newTransfer v-model="value" :data="data" :virtual-scroll="true"></newTransfer></div>
</template><script setup lang="ts">
import { ref } from 'vue'
import newTransfer from './transfer/index.js'const generateData = () => {const data = []for (let i = 1; i <= 149998; i++) {data.push({key: i,label: `Option ${i}`,disabled: i % 4 === 0,})}return data
}const data = ref(generateData())
const value = ref([])
</script>

4、页面展示为:

// 注意:一定要安装命令: npm i vue-virtual-scroll-list,才能实现虚拟滚动;

// 此时可以随时从 Option 1 到 Option 149998 条数据;
在这里插入图片描述

// 全选也没有问题,几乎立即可以全选;

在这里插入图片描述
// 全选大量数据也可以短时间内,向右侧 transfer 过去;

在这里插入图片描述

Ⅵ、小结:

其一、哪里有不对或不合适的地方,还请大佬们多多指点和交流!
其二、若有转发或引用本文章内容,请注明本博客地址(直接点击下面 url 跳转) https://blog.csdn.net/weixin_43405300,创作不易,且行且珍惜!
其三、有兴趣的话,可以多多关注这个专栏(Vue(Vue2+Vue3)面试必备专栏)(直接点击下面 url 跳转):https://blog.csdn.net/weixin_43405300/category_11525646.html?spm=1001.2014.3001.5482

这篇关于(Transfer)解决:Element-ui 中 Transfer 穿梭框因数据量过大而渲染卡顿问题的三种方法的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/887889

相关文章

HarmonyOS学习(七)——UI(五)常用布局总结

自适应布局 1.1、线性布局(LinearLayout) 通过线性容器Row和Column实现线性布局。Column容器内的子组件按照垂直方向排列,Row组件中的子组件按照水平方向排列。 属性说明space通过space参数设置主轴上子组件的间距,达到各子组件在排列上的等间距效果alignItems设置子组件在交叉轴上的对齐方式,且在各类尺寸屏幕上表现一致,其中交叉轴为垂直时,取值为Vert

大模型研发全揭秘:客服工单数据标注的完整攻略

在人工智能(AI)领域,数据标注是模型训练过程中至关重要的一步。无论你是新手还是有经验的从业者,掌握数据标注的技术细节和常见问题的解决方案都能为你的AI项目增添不少价值。在电信运营商的客服系统中,工单数据是客户问题和解决方案的重要记录。通过对这些工单数据进行有效标注,不仅能够帮助提升客服自动化系统的智能化水平,还能优化客户服务流程,提高客户满意度。本文将详细介绍如何在电信运营商客服工单的背景下进行

基于MySQL Binlog的Elasticsearch数据同步实践

一、为什么要做 随着马蜂窝的逐渐发展,我们的业务数据越来越多,单纯使用 MySQL 已经不能满足我们的数据查询需求,例如对于商品、订单等数据的多维度检索。 使用 Elasticsearch 存储业务数据可以很好的解决我们业务中的搜索需求。而数据进行异构存储后,随之而来的就是数据同步的问题。 二、现有方法及问题 对于数据同步,我们目前的解决方案是建立数据中间表。把需要检索的业务数据,统一放到一张M

关于数据埋点,你需要了解这些基本知识

产品汪每天都在和数据打交道,你知道数据来自哪里吗? 移动app端内的用户行为数据大多来自埋点,了解一些埋点知识,能和数据分析师、技术侃大山,参与到前期的数据采集,更重要是让最终的埋点数据能为我所用,否则可怜巴巴等上几个月是常有的事。   埋点类型 根据埋点方式,可以区分为: 手动埋点半自动埋点全自动埋点 秉承“任何事物都有两面性”的道理:自动程度高的,能解决通用统计,便于统一化管理,但个性化定

使用SecondaryNameNode恢复NameNode的数据

1)需求: NameNode进程挂了并且存储的数据也丢失了,如何恢复NameNode 此种方式恢复的数据可能存在小部分数据的丢失。 2)故障模拟 (1)kill -9 NameNode进程 [lytfly@hadoop102 current]$ kill -9 19886 (2)删除NameNode存储的数据(/opt/module/hadoop-3.1.4/data/tmp/dfs/na

异构存储(冷热数据分离)

异构存储主要解决不同的数据,存储在不同类型的硬盘中,达到最佳性能的问题。 异构存储Shell操作 (1)查看当前有哪些存储策略可以用 [lytfly@hadoop102 hadoop-3.1.4]$ hdfs storagepolicies -listPolicies (2)为指定路径(数据存储目录)设置指定的存储策略 hdfs storagepolicies -setStoragePo

Hadoop集群数据均衡之磁盘间数据均衡

生产环境,由于硬盘空间不足,往往需要增加一块硬盘。刚加载的硬盘没有数据时,可以执行磁盘数据均衡命令。(Hadoop3.x新特性) plan后面带的节点的名字必须是已经存在的,并且是需要均衡的节点。 如果节点不存在,会报如下错误: 如果节点只有一个硬盘的话,不会创建均衡计划: (1)生成均衡计划 hdfs diskbalancer -plan hadoop102 (2)执行均衡计划 hd

好题——hdu2522(小数问题:求1/n的第一个循环节)

好喜欢这题,第一次做小数问题,一开始真心没思路,然后参考了网上的一些资料。 知识点***********************************无限不循环小数即无理数,不能写作两整数之比*****************************(一开始没想到,小学没学好) 此题1/n肯定是一个有限循环小数,了解这些后就能做此题了。 按照除法的机制,用一个函数表示出来就可以了,代码如下

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象