关于element-ui Transfer 穿梭框数据大卡的问题

153 阅读2分钟

关于element-ui Transfer 穿梭框数据大卡的问题

我最近用到vue2 element-ui Transfer 穿梭框数据太大导致穿梭框很卡,页面渲染很久,有时候导致浏览器挂机。

网上看咯一堆解决方案,最后完美解决这个问题,废话不多少,直接上代码吧!

1.先安装这个,我这里使用2.3.3版本
npm i vue-virtual-scroll-list
​
"dependencies": {
  "vue-virtual-scroll-list": "^2.3.3",
}
     
"devDependencies": {
 "vue-virtual-scroll-list": "^2.3.3"
}
2.然后就是组件代码共3个文件
1.创建transfer-checkbox-item.vue
<template>
  <el-checkbox
    class="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 item
      type: 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>
2.创建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">
        <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-checkbox
            class="el-transfer-panel__item"
            :label="item[keyProp]"
            :disabled="item[disabledProp]"
            :key="index"
            v-for="(item,index) in filteredData">
            <option-content :option="item"></option-content>
          </el-checkbox>
        </template>
      </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';
import Item from './transfer-checkbox-item.vue';
import VirtualList from 'vue-virtual-scroll-list';
​
export default {
  mixins: [Locale],
​
  name: 'ElTransferPanel',
​
  componentName: 'ElTransferPanel',
​
  components: {
    'virtual-list': VirtualList,
    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,
      itemComponent: Item,
      virtualListProps: {}
    };
  },
​
  watch: {
    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: {
    virtualScroll() {
      return this.$parent.virtualScroll;
    },
    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() {
      this.virtualListProps.keyProp = this.props.key || 'key';
      return this.props.key || 'key';
    },
​
    disabledProp() {
      this.virtualListProps.disabledProp = this.props.disabled || 'disabled';
      return this.props.disabled || 'disabled';
    },
​
    hasFooter() {
      return !!this.$slots.default;
    }
  },
​
  methods: {
    load() {
      // 当用户滚动到列表的底部时,额外渲染多50条
      this.count += 50
    },
    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>
​
3.创建main.vue
<template>
  <div class="el-transfer">
    <transfer-panel
      v-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-button
        type="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-button
        type="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-panel
      v-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: {
    virtualScroll: {
      type: Boolean,
      default: false
    },
    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, {});
    },
​
    sourceData() {
      let valueObj = {};
      this.value.forEach((item) => {
        valueObj[item] = true;
      });
      return this.data.filter(
        (item) => !valueObj[item[this.props.key]]
      );
    },
​
    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);
    },
    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.导入组件,我是在main.js导入
// Transfer 穿梭框
import Transfer from '@/components/Transfer/main'
Vue.component('Transfer', Transfer)
​
4.使用
<template>
  <div>
    <Transfer v-model="value" :data="data" :virtual-scroll="true" :button-texts="['到左边', '到右边']"></Transfer>
  </div>
</template><script>
export default {
  data() {
    const generateData = _ => {
      const data = [];
      for (let i = 1; i <= 100000; i++) {
        data.push({
          key: i,
          label: `备选项 ${i}`,
        });
      }
      return data;
    };
    return {
      data: generateData(),
      value: []
    };
  }
}
</script>