使用element 2.14 实现表格虚拟滚动组件

发布时间 2023-12-13 16:38:06作者: 2000New

下述代码为组件实现代码复制即可食用,默认只展示 一屏数据加两条

全选存在些许问题, 使用row-key时,如果行过多滚动时会不会很流畅

特别需要注意的是 行高必须要保持一致

<template>
  <div class="t-table" :id="TTableId">
    <el-table
      ref="el-table"
      :tooltip-effect="tooltipEffect"
      :data="tableData"
      border
      :height="height"
      :row-class-name="rowClassName"
      :key="tableKey"
      :row-key="getRowKey"
      @select-all="
        val => {
          this.selectAll(val, !isAll);
        }
      "
      @select="select"
      @current-change="handleCurrentChange"
      :highlight-current-row="highlightCurrentRow"
      :header-cell-style="headerCellStyle"
      :cell-style="cellStyle"
      :empty-text="emptyText"
      @row-click="
        (row, column, event) => this.rowClick(row, column, event, 'table')
      "
    >
      <el-table-column type="index" v-if="index" align="center" fixed>
        <template slot-scope="scope">
          {{ start > 0 ? scope.$index + start + 1 : scope.$index + 1 }}
        </template>
      </el-table-column>
      <el-table-column
        type="selection"
        fixed
        :reserve-selection="true"
        :width="$TableWidth.px46"
        v-if="selection"
        :selectable="rowSelectable"
      ></el-table-column>
      <slot></slot>
    </el-table>
  </div>
</template>

<script>
export default {
  components: {
  },
  name: "TTable",
  props: {
    //表格数据列表
    saveDATA: {
      typeof: Array,
      default: []
    },
    //表格高度
    height: {
      typeof: Number,
      default: 400
    },
    //一行高度 为了计算高度,需要行高固定且不能出现换行数据
    itemHeight: {
      typeof: Number,
      default: 49
    },

    rowClassName: {
      typeof: Function,
      default: () => {}
    },
    tooltipEffect: {
      typeof: String,
      default: ""
    },
    //行key 必须填写saveDATA里面的变量名
    rowKey: {
      typeof: String,
      default: ""
    },
    //是否显示复选框列
    selection: {
      typeof: Boolean,
      default: false
    },
    //是否显示序号列
    index: {
      typeof: Boolean,
      default: false
    },
    //行禁选方法
    rowSelectable: {
      typeof: Function,
      default: () => {}
    },
    //是否可以单选
    highlightCurrentRow: {
      typeof: Boolean,
      default: false
    },
    //表头样式
    headerCellStyle: {
      typeof: Object
    },
    //表格样式
    cellStyle: {
      typeof: Object
    },
    //无数据文字
    emptyText: {
      typeof: String,
      default: ""
    },
    colDisable: {
      typeof: Array
    },
    //组件id 如果一个页面需要使用多个ttable组件 需要给此参数赋唯一值
    TTableId: {
      typeof: String,
      default: "t_table"
    }
  },
  data() {
    return {
      tableData: [], //展示数据列表
      tableRef: null, // 设置了滚动的那个盒子
      tableWarp: null, // 被设置的transform元素
      fixLeft: null, // 固定左侧--设置的transform元素
      fixRight: null, // 固定右侧--设置的transform元素
      tableFixedLeft: null, // 左侧固定列所在的盒子
      tableFixedRight: null, // 右侧固定列所在的盒子
      scrollTop: 0, //滚动高度
      scrollNum: 0, //当前滚动了几屏数据  scrollTop / (itemHeight * pageList)
      scrollRowNum: 0, //记录当前滚动了多少条数据
      start: 0, //默认截取数据开始位数
      end: this.pageList + 2, //默认截取数据结束位数
      selectList: [], //当前选中的列表
      isAll: false, //是否全选
      tableKey: "", //表格key
      currentRow: "", //单选数据
    };
  },
  watch: {
    //每滚动一条数据则更改展示数据源并且更改容器偏移量
    scrollRowNum(newV) {
      this.start = newV - 1;
      this.end = this.pageList + newV + 1;
      if (this.start <= 0) this.start = 0;
      this.tableData = this.saveDATA.slice(this.start, this.end);
      if (this.currentRow) this.setCurrentRow(this.currentRow);
      requestAnimationFrame(() => {
        // 计算偏移量
        let translateY = `translateY(${this.start * this.itemHeight}px)`;
        this.tableWarp.style.transform = translateY;
        if (this.fixLeft) {
          this.fixLeft.style.transform = translateY;
        }
        if (this.fixRight) {
          this.fixRight.style.transform = translateY;
        }
        this.doLayout();
      });
    },
 
    saveDATA: {
      handler(val) {
        this.init();
        this.initMounted();
      },
      deep: true // 深度监听
    },
    pageList() {
      this.init();
      this.initMounted();
    }
  },
  created() {
  },
  mounted() {
    this.$nextTick(() => {
      this.tableSetRef = this.GetTableRef();
    });
  },
  methods: {
    clearIndex() {
      this.start = 0;
      this.end = this.pageList + 2; 
      this.scrollNum = 0;
      if (this.tableRef) this.tableRef.scrollTop = 0;
    },
    //初始化表格内容盒子
    initMounted() {
      this.$nextTick(() => {
        let box = document.getElementById(this.TTableId);
        // 设置了滚动的盒子
        this.tableRef = this.$refs["el-table"].bodyWrapper;
        // 左侧固定列所在的盒子
        this.tableFixedLeft = box.querySelector(
          ".el-table .el-table__fixed .el-table__fixed-body-wrapper"
        );
        // 右侧固定列所在的盒子
        this.tableFixedRight = box.querySelector(
          ".el-table .el-table__fixed-right .el-table__fixed-body-wrapper"
        );
        /**
         * fixed-left | 主体 | fixed-right
         */
        // 创建内容盒子divWarpPar并且高度设置为所有数据所需要的总高度
        let dwp = box.getElementsByClassName("divWarpPar")[0];
        if (dwp) {
          dwp.style.height = this.saveDATA.length * this.itemHeight + "px";
        } else {
          let divWarpPar = document.createElement("div");
          divWarpPar.className = "divWarpPar";
          // 如果这里还没获取到saveDATA数据就渲染会导致内容盒子高度为0,可以通过监听saveDATA的长度后再设置一次高度
          divWarpPar.style.height =
            this.saveDATA.length * this.itemHeight + "px";
          // 新创建的盒子divWarpChild
          let divWarpChild = document.createElement("div");
          divWarpChild.className = "fix-warp divWarpChild";
          // 把tableRef的第一个子元素移动到新创建的盒子divWarpChild中
          divWarpChild.append(this.tableRef.children[0]);
          // 把divWarpChild添加到divWarpPar中,最把divWarpPar添加到tableRef中
          divWarpPar.append(divWarpChild);
          this.tableRef.append(divWarpPar);
        }
        // left改造
        let dlp = box.getElementsByClassName("divLeftPar")[0];
        if (dlp) {
          dlp.style.height = this.saveDATA.length * this.itemHeight + "px";
        } else {
          let divLeftPar = document.createElement("div");
          divLeftPar.className = "divLeftPar";
          divLeftPar.style.height =
            this.saveDATA.length * this.itemHeight + "px";
          let divLeftChild = document.createElement("div");
          divLeftChild.className = "fix-left";
          this.tableFixedLeft &&
            divLeftChild.append(this.tableFixedLeft.children[0]);
          divLeftPar.append(divLeftChild);
          this.tableFixedLeft && this.tableFixedLeft.append(divLeftPar);
        }
        // right改造
        let drp = box.getElementsByClassName("divRightPar")[0];
        if (drp) {
          drp.style.height = this.saveDATA.length * this.itemHeight + "px";
        } else {
          let divRightPar = document.createElement("div");
          divRightPar.className = "divRightPar";
          divRightPar.style.height =
            this.saveDATA.length * this.itemHeight + "px";
          let divRightChild = document.createElement("div");
          divRightChild.className = "fix-right";
          this.tableFixedRight &&
            divRightChild.append(this.tableFixedRight.children[0]);
          divRightPar.append(divRightChild);
          this.tableFixedRight && this.tableFixedRight.append(divRightPar);
        }

        // 被设置的transform元素
        this.tableWarp = box.querySelector(
          ".el-table .el-table__body-wrapper .fix-warp"
        );
        this.fixLeft = box.querySelector(
          ".el-table .el-table__fixed .el-table__fixed-body-wrapper .fix-left"
        );
        this.fixRight = box.querySelector(
          ".el-table .el-table__fixed-right .el-table__fixed-body-wrapper .fix-right"
        );
        this.tableRef.addEventListener("scroll", this.onScroll);
      });
    },
    // 初始化数据
    init() {
      this.clearIndex();
      this.tableData = this.saveDATA.slice(this.start, this.end);
    },
    // 滚动事件
    onScroll() {
      //解决固定行 与 非固定行 滚动时错位问题
      let body = document.getElementById(this.TTableId);
      let fixedBox = body.getElementsByClassName(
        "el-table__fixed-body-wrapper"
      );
      if (fixedBox) {
        for (let i = 0; i < fixedBox.length; i++) {
          const element = fixedBox[i];
          element.scrollTop = this.tableRef.scrollTop;
        }
      }
      //,
      this.scrollTop = this.tableRef.scrollTop; //获取顶部距离
      this.scrollNum =
        Math.floor(this.scrollTop / (this.itemHeight * this.pageList)) + 1; //计算滚动到几屏数据
      this.scrollRowNum = Math.ceil(this.scrollTop / this.itemHeight); //计算已经滚动了多少条数据
      //移除掉滚动时遗留的hover-row
      if (this.GetTableRef()) {
        const elements = body.getElementsByClassName(
          "hover-row"
        );
        for (let i = 0; i < elements.length; i++) {
          elements[i].classList.remove("hover-row");
        }
      }
    },
    //获取表格ref
    GetTableRef() {
      return this.$refs["el-table"];
    },
    //刷新表格key
    RefreshTableKey() {
      this.tableKey = this.$common.GetNextStr();
      this.clearIndex();
      this.initMounted();
    },
    //设置行key
    getRowKey(row) {
      return this.$common.GetValueByPath(row, this.rowKey);
    },
    //单行选择触发事件
    select(val) {
      this.selectList = val;
      this.$emit("selection-change", val);
    },
    //全选触发事件
    selectAll(val, isAll) {
      this.isAll = isAll;
      if (isAll) {
        let list = JSON.parse(JSON.stringify(this.saveDATA));
        if (this.rowSelectable)
          this.selectList = list.filter((m, i) => this.rowSelectable(m, i));
        else this.selectList = list;
        let dList = this.selectList.filter(
          m => val.findIndex(k => JSON.stringify(k) == JSON.stringify(m)) == -1
        );
        dList.forEach(row => {
          this.GetTableRef().toggleRowSelection(row);
        });
      } else {
        this.clearSelection();
      }
      this.$emit("selection-change", this.selectList);
    },
    //清空表格
    clearSelection() {
      this.GetTableRef().clearSelection();
      this.selectList = [];
      this.$emit("selection-change", this.selectList);
    },

    //单行选中触发事件
    handleCurrentChange(val) {
      this.currentRow = val;
      this.$emit("current-change", val);
    },
    //设置指定行为单行选中
    setCurrentRow(row) {
      this.currentRow = row;
      this.GetTableRef().setCurrentRow(row);
    },
    rowClick(row, column, event) {
      this.$emit("row-click", row, column, event);
    },
    //重新布局表格
    doLayout() {
      this.GetTableRef().doLayout();
    },


  },
  computed: {
    pageList() {
      return Math.ceil(this.height / this.itemHeight);
    }
  }
};
</script>
<style>
.t-table .el-table__body-wrapper {
  overflow: auto !important;
  background-blend-mode: overlay !important;
}
</style>

下面为调用示例

<template>
  <div class="t-table" id="t_table">
    <TTable :saveDATA="saveDATA" :height="200">
      <el-table-column fixed label="id" prop="id"></el-table-column>
      <el-table-column  fixed label="姓名" prop="name"></el-table-column>
      <el-table-column label="年龄" prop="age"></el-table-column>
      <el-table-column
        label="地址"
        prop="address"
        min-width="300"
      ></el-table-column>
      <el-table-column
        label="地址"
        prop="address"
        min-width="300"
      ></el-table-column>
      <el-table-column
        label="地址"
        prop="address"
        min-width="300"
      ></el-table-column>
      <el-table-column
        label="地址"
        prop="address"
        min-width="300"
      ></el-table-column>
      <el-table-column
        label="地址"
        prop="address"
        min-width="300"
      ></el-table-column>
      <el-table-column
        label="地址"
        prop="address"
      min-width="300"
      ></el-table-column>
    </TTable>
  </div>
</template>

<script>
import TTable from "../../../src/components/TTable.vue";
export default {
  components: {
    TTable,
  },
  props: {},
  data() {
    return {
      saveDATA: [], // 所有数据
    };
  },
  watch: {},
  created() {

  },
  mounted() {
    this.init();
  },
  methods: {
    init() {
      this.saveDATA = [];

      for (let i = 0; i < 2000; i++) {
        this.saveDATA.push({
          id: i,
          name: `张三${i}`,
          age: i,
          address: "重庆市渝中区两路口阿士大夫撒多富士达浪费的水立方as的裂缝阿萨德立法技术放两三分啥地方了as的裂缝as了巅峰就啥的立法啥的立法阿斯蒂芬了as的裂缝就啥砥砺奋进阿萨德理发店发了"
        });
      }
    }
  }
};
</script>