【虚拟滚动】vue2 element el-table组件虚拟滚动效果

164 阅读3分钟

首先,虚拟滚动的场景是什么? 项目中可能会遇到一些通用情况,它要求列表不能分页处理,也不能懒加载,必须一次性展示“所有数据”,且不能有过分的卡顿等问题。

1. 行业常见的报警列表,实时性要求很高,必须全部展示,且不可分页。

2. 后端提供的接口为一个list,没做分页处理,也没有按关键字查询接口,只能前端自行解决页面渲染和查询。

这次就以第二种情况来说明如何处理,接上一篇博客 一文搞懂web端国际化方案(以vue2+elementUI为例,包含远程获取国际化)

为了给多语言提供一个可供展示和修改的页面,前端自己实现了一个虚拟滚动且能修改多语言的页面。

在这里插入图片描述 直接上代码:

    <div class="gl-cell-card-box p-10 bg_white">
        <el-table
          v-loading="tableLoading"
          v-bind="_options"
          ref="tableRef"
          max-height="600"
          :data="sliceTable"
          :row-key="row => row.id"
          @select="handleSelect"
          @select-all="handleSelectAll"
        >
            <!-- <el-table-column type="index" width="40"> </el-table-column> -->
            <el-table-column prop="module" label="模块" width="120"></el-table-column>
            <el-table-column prop="app" label="所属端"></el-table-column>
            <el-table-column prop="key" label="key"></el-table-column>
            <el-table-column prop="value" label="值">
              <template #default="{ row }">
                <el-input 
                  v-if="editing[row.key]" 
                  v-model="row.value"
                  ref="inputRef"
                  size="small"
                  @blur="(e) => blur(e, row)"
                >
                </el-input>
                <span 
                  v-else 
                  @click="editRow(row)"
                >
                 {{ row.value }}
                </span>
              </template>
            </el-table-column>
        </el-table>
    </div>
import { 
  getI18nMessageSource, 
  updateI18nMessage 
} from '../../api/api';
import _ from 'lodash';
const showNums = 12;
let originTableData = [];
export default {
  name: 'languageMain',
  data() {
    return {
      // ITable配置
   tableData: [],
      tableLoading: false,
      // 开始索引
      startIndex: 0,
      // 选中的数据
      selectedRows: [],
      // 空元素,用于撑开table的高度
      vEle: undefined,
      // 是否全选
      isSelectedAll: false,
      editing: {},
      // currentValue: {},
      currentRow: {},
    }
  },
  computed: {
    // 这个是截取表格中的部分数据,放到了 table 组件中来显示
    sliceTable: {
        get() {
          return this.tableData.slice(
            this.startIndex,
            (
              (this.tableData.length - this.startIndex) > showNums) ? 
                (this.startIndex + showNums) 
                  : 
                  this.tableData.length
            );
        },
        set(val) {
          this.tableData = val;
        }
    },
    _options() {
      const option = {
        border: false, 
        stripe: false, 
        headerCellStyle: { background: '#e9e9ef', color: '#515a6e', }, 
        tooltipEffect: "dark",
        showHeader: true,
        showPagination: false,
        rowStyle: () => "cursor:pointer", // 行样式
      };
      return option;
    },
  },
  created() {
    // 创建一个空元素,这个空元素用来撑开 table 的高度,模拟所有数据的高度
    this.vEle = document.createElement("div");
  },
  mounted() {
    this.getLanguage()
    // 绑定滚动事件
    this.$refs.tableRef.$el
      .querySelector(".el-table__body-wrapper")
      .addEventListener("scroll", this.tableScroll, {
      passive: true
    });
  },
  methods: {
     searchModelFun() {
      this.tableData = _.cloneDeep(originTableData);
      if(!this.searchModel.value) {
        return;
      }
      const { key, value } = this.searchModel;
      const filterData = this.tableData.filter(item => item[key].includes(value));
      this.tableData = filterData;
    },
    getLanguage(language = 'zh-CN') {
      this.tableLoading = true;
      getI18nMessageSource(this.searchForm).then(res=> {
        const { success, data } = res
        if (success) {
          if(!_.isEmpty(data)) {
            this.tableData = [];
            Object.keys(data).forEach(key => {
              this.tableData.push({
                id: key,
                module: this.searchForm.module,
                app: this.searchForm.app,
                key: key,
                value: data[key]
              })
            })
            // 存储原始数据,用于筛选重置
            originTableData = _.cloneDeep(this.tableData);
            // 重新加载数据,重新计算高度
            this.loadData();
          }
        } else {
          console.log('获取国际化失败')
        }
      }).catch(err => {
        console.log('获取国际化失败err', err)
      }).finally(() => {
        this.tableLoading = false;
      })
    },
    editRow(row) {
      this.editing[this.currentRow.key] = false;
      this.currentRow = _.cloneDeep(row);
      if (this.editing[row.key] === false) {
        this.editing[row.key] = true;
        this.$nextTick(()=> {
          this.$refs.inputRef.focus();
        })
        return;
      }
      this.$set(this.editing, row.key, true);
      this.$nextTick(()=> {
        this.$refs.inputRef.focus();
      })
    },
    blur(e, row) {
      this.editing[row.key] = false;
      // 如果没有变化,则不更新数据
      if (this.currentRow.value === row.value) {
        this.currentRow = {};
        return; 
      }
      const params = {
        app: this.searchForm.app,
        module: this.searchForm.module,
        language: this.activeName,
        label: row.key,
        value: row.value,
      }
      this.updateLanguage(params)
    },
    updateLanguage(params) {
      updateI18nMessage(params).then(res => {
        const { success, data } = res
        if (success) {
          this.getLanguage()
          this.$message({
            type: 'success',
            message: '更新成功!'
          });
        } else {
          this.$message({
            type: 'success',
            message: '更新失败!'
          });
        }
      }).catch(err => {
        this.$message({
          type: 'success',
          message: '更新失败~'
        });
      })
    },
    // 加载数据
    loadData() {
        this.$nextTick(() => {
            // 设置成绝对定位,这个元素需要我们去控制滚动
            this.$refs.tableRef.$el.querySelector(".el-table__body").style.position = "absolute";
            // 计算表格所有数据所占内容的高度
            this.vEle.style.height = this.tableData.length * 48 + "px";
            // 把这个节点加到表格中去,用它来撑开表格的高度
            this.$refs.tableRef.$el.querySelector(".el-table__body-wrapper").appendChild(this.vEle);
            // 重新设置曾经被选中的数据
            this.selectedRows.forEach(row => {
                this.$refs.tableRef.toggleRowSelection(row, true);
            });
        });
    },
    /**
     * @description: 手动勾选时的事件
     * @param {*} selection - 选中的所有数据
     * @param {*} row - 当前选中的数据
     * @return {*}
     */
    handleSelect(selection, row) {
        this.selectedRows = selection;
    },
    /**
     * @description: 全选事件
     * @param {*} selection
     * @return {*}
     */
    handleSelectAll(selection) {
        this.isSelectedAll = !this.isSelectedAll;
        if (this.isSelectedAll) {
            this.selectedRows = this.tableData;
        } else {
            this.selectedRows = [];
            this.$refs.tableRef.clearSelection();
        }
    },
    /**
     * @description: table 滚动事件
     * @param {*}
     * @return {*}
     */
    tableScroll() {
        let bodyWrapperEle = this.$refs.tableRef.$el.querySelector(".el-table__body-wrapper");
        // 滚动的高度
        let scrollTop = bodyWrapperEle.scrollTop;
        // 下一次开始的索引
        this.startIndex = Math.floor(scrollTop / 48);
        // console.log(scrollTop, this.startIndex,'scrollTop, startIndex');
        // 滚动操作
        bodyWrapperEle.querySelector(".el-table__body").style.transform = `translateY(${this.startIndex * 48}px)`;
        // 滚动操作后,上面的一些 tr 没有了,所以需要重新设置曾经被选中的数据
        this.$nextTick(() => {
            this.selectedRows.forEach(row => {
                this.$refs.tableRef.toggleRowSelection(row, true);
            });
        })
        // 滚动到底,加载新数据
        if (bodyWrapperEle.scrollHeight <= scrollTop + bodyWrapperEle.clientHeight) {
            this.$message.warning("没有更多了");
            return;
        }
    }
  },
};

总结:

1.创建vEle空div元素,利用接口获取所有数据,将原始数据存于originTableData,并按照showNums计算div实际高度,将 vEle塞入.el-table__body-wrapper中,撑开table,使其拥有滚动条。

2.监听el-table__body-wrapper的滚动事件,动态获取当前滚动的高度,计算出当前应该展示的table数据的首项(此时computed中sliceTable会自动计算出要渲染的数据项,并回显到table组件中),最后将el-table__body利用position:absolute;计算定位距离translateY滚到视口位置防止白屏。

3.如果涉及选择项selection,那么每次滚动后都要重新nextTick,设置一次toggleRowSelection选择,防止出现视图问题。