vue+elementui虚拟滚动(包含树状结构)

48 阅读2分钟

vue虚拟滚动例子(vue2)

一. vue2 简单虚拟滚动始终是20条数据

<template>
  <div
    class="virtual-scroll-container"
    @scroll="handleScroll"
    :style="{ height: containerHeight + 'px' }"
  >
    <!-- 表头 -->
    <el-table :data="[]" class="virtual-header">
      <el-table-column prop="name" label="姓名"></el-table-column>
      <el-table-column prop="age" label="年龄"></el-table-column>
    </el-table>
​
    <!-- 虚拟滚动内容 -->
    <div class="virtual-content" :style="{ height: totalHeight + 'px' }">
      <!-- :style="{ transform: `translateY(${offset}px)` }" -->
      <div
        class="virtual-list"
        :style="{ position: 'absolute', top: `${offset}px`, left: 0 }"
      >
        <el-table
          :show-header="false"
          :data="visibleData"
          :row-style="{ height: rowHeight + 'px' }"
          style="width: 100%"
          class="virtual-body"
        >
          <el-table-column prop="name" label="姓名"></el-table-column>
          <el-table-column prop="age" label="年龄"></el-table-column>
        </el-table>
      </div>
    </div>
  </div>
</template><script>
export default {
  data() {
    return {
      allData: [], // 全部数据
      visibleData: [], // 当前显示的数据(固定20条)
      rowHeight: 40, // 每行高度
      containerHeight: 800, // 容器可视高度
      startIndex: 0 // 当前起始索引
    }
  },
  computed: {
    // 总滚动高度(所有数据高度)
    totalHeight() {
      return this.allData.length * this.rowHeight
    },
    // 内容偏移量
    offset() {
      return this.startIndex * this.rowHeight
    }
  },
  methods: {
    // 滚动事件处理
    handleScroll(e) {
      const scrollTop = e.target.scrollTop
      this.startIndex = Math.floor(scrollTop / this.rowHeight)
      this.updateVisibleData()
    },
    // 更新显示数据
    updateVisibleData() {
      this.visibleData = this.allData.slice(
        this.startIndex,
        this.startIndex + 20
      )
    }
  },
  mounted() {
    // 生成测试数据(1000条示例)
    this.allData = Array.from({ length: 1000 }, (_, i) => ({
      id: i,
      name: `用户${i + 1}`,
      age: 20 + (i % 30)
    }))
    this.updateVisibleData()
  }
}
</script><style lang="scss" scoped>
.virtual-header {
  width: 100%;
  position: sticky;
  top: 0;
  z-index: 1;
  pointer-events: none;
  ::v-deep .goods-table__empty-block {
    display: none;
  }
}
.virtual-scroll-container {
  position: relative;
  overflow-y: auto;
  border: 1px solid #ebeef5;
}
​
/* 隐藏空表格的默认提示 */
.virtual-body .el-table__empty-block {
  display: none;
}
​
/* 隐藏表头 */
.hidden-header {
  display: none;
}
​
.virtual-content {
  position: relative;
}
​
.virtual-list {
  position: absolute;
  width: 100%;
  top: 0;
  left: 0;
}
​
/* 调整表格单元格高度 */
.virtual-body .el-table__row td {
  padding: 0;
  height: 40px;
  line-height: 40px;
}
</style>

二、vue2 +树状结构的虚拟滚动

注意事项: table结构一旦扁平话,就会破坏树状的功能逻辑,父级的展开收起按钮就不会存在了 default-expand-all也会失效,这个时候展开收起功能就需要自定义,代码逻辑如下

<template>
  <div class="virtual-tree-table" @scroll="handleScroll" ref="scrollContainer">
    <!-- 虚拟滚动占位 -->
    <!-- 表头 -->
    <el-table :data="[]" class="virtual-header">
      <el-table-column prop="label" label="节点名称"></el-table-column>
      <el-table-column prop="size" label="节点大小"></el-table-column>
      <el-table-column prop="date" label="节点日期"></el-table-column>
    </el-table>
    <div
      class="scroll-space"
      :style="{ height: totalHeight + 'px', position: 'relative' }"
    >
      <!-- 固定显示20行的可视表格 -->
      <div class="virtual-tree-table-content" :style="styleCustom">
        <el-table
          :show-header="false"
          :data="visibleData"
          :row-key="rowKey"
          :tree-props="treeProps"
          :default-expand-all="true"
        >
          <el-table-column prop="label" label="节点名称">
            <template slot-scope="{ row }">
              <div :style="getIndentStyle(row)">
                <i
                  v-if="row.hasChildren"
                  class="el-icon-arrow-right"
                  :class="{ expanded: row.expanded }"
                  @click="toggleExpand(row)"
                ></i>
                <span>{{ row.label }}</span>
              </div>
            </template>
          </el-table-column>
          <el-table-column prop="size" label="节点大小"></el-table-column>
          <el-table-column prop="date" label="节点日期"></el-table-column>
        </el-table>
      </div>
    </div>
  </div>
</template>
<script>
export default {
  data() {
    return {
      flatData: [], // 扁平化的可见节点列表
      visibleData: [], // 当前显示的20条数据
      startIndex: 0, // 当前起始索引
      scrollTop: 0, // 当前滚动位置
      rawTreeData: [],
      offset: 0,
      rowKey: 'id',
      rowHeight: 42,
      treeProps: {
        // 树形配置
        children: 'children',
        hasChildren: 'hasChildren'
      }
    }
  },
  computed: {
    styleCustom() {
      return {
        position: 'absolute',
        top: `${this.offset}px`,
        width: '100%'
      }
    },
    // 总高度计算(所有可见节点)
    totalHeight() {
      return this.flatData.length * this.rowHeight
    },
    // 当前显示范围(带缓冲区)
    visibleRange() {
      return [
        Math.max(0, this.startIndex),
        Math.min(this.flatData.length, this.startIndex + 20)
      ]
    }
  },
  methods: {
    generateDemoData(depth = 3, breadth = 100, level = 0, prefix = '') {
      if (level >= depth) return []
​
      const list = Array.from({ length: breadth }).map((_, i) => {
        const id = prefix ? `${prefix}-${i}` : `${i}`
​
        return {
          id,
          label: `文件夹 ${id}`,
          size: Math.floor(Math.random() * 1000) + ' KB',
          date: new Date(
            Date.now() - Math.random() * 10000000000
          ).toLocaleDateString(),
          children: this.generateDemoData(depth, breadth, level + 1, id),
          expanded: true
        }
      })
​
      return list
    },
​
    // 处理原始树数据(核心方法)
    processTreeData() {
      this.flatData = this.flattenTree(this.rawTreeData)
      this.updateVisibleData()
    },
​
    // 递归扁平化树结构
    flattenTree(nodes, level = 0) {
      return nodes.reduce((acc, node) => {
        const flatNode = {
          ...node,
          level,
          expanded: node.expanded,
          hasChildren: node.children && node.children.length > 0
        }
        acc.push(flatNode)
        if (flatNode.children && flatNode.expanded) {
          acc.push(...this.flattenTree(node.children, level + 1))
        }
        return acc
      }, [])
    },
​
    // 更新显示数据(固定20条)
    updateVisibleData() {
      const [start, end] = this.visibleRange
      const dataSlice = this.flatData.slice(start, end)
      this.offset = start * this.rowHeight
      // 填充空白数据保持20条
      this.visibleData = [...dataSlice].slice(0, 20)
    },
​
    // 行样式控制
    rowStyle({ row }) {
      return {
        height: `${this.rowHeight}px`,
        display: row.id ? 'table-row' : 'none' // 隐藏填充行
      }
    },
​
    // 缩进样式
    getIndentStyle(row) {
      return row.id
        ? {
            paddingLeft: `${row.level * 20 + 10}px`,
            display: 'flex',
            alignItems: 'center'
          }
        : {}
    },
​
    // 展开/折叠切换
    toggleExpand(row) {
      row.expanded = !row.expanded
      this.rawTreeData.forEach(item => {
        if (item.id === row.id) {
          item.expanded = row.expanded
        }
      })
      this.processTreeData()
      this.$nextTick(() => {
        this.adjustScrollPosition(row)
      })
    },
​
    // 滚动位置调整(展开后保持视觉连续性)
    adjustScrollPosition(row) {
      const index = this.flatData.findIndex(
        n => n[this.rowKey] === row[this.rowKey]
      )
      const visiblePosition = index - this.startIndex
      if (visiblePosition < 0 || visiblePosition > 19) {
        this.startIndex = Math.max(0, index - 10)
        this.updateVisibleData()
      }
    },
​
    // 滚动事件处理
    handleScroll(e) {
      this.scrollTop = e.target.scrollTop
      this.startIndex = Math.floor(
        (this.scrollTop / this.totalHeight) * this.flatData.length
      )
      this.updateVisibleData()
    }
  },
  mounted() {
    // 初始填充20条数据
    this.rawTreeData = this.generateDemoData(2, 10)
    this.processTreeData()
  }
}
</script><style lang="scss" scoped>
.virtual-header {
  width: 100%;
  position: sticky;
  top: 0;
  z-index: 1;
  pointer-events: none;
  ::v-deep .goods-table__empty-block {
    display: none;
  }
}
.virtual-tree-table {
  position: relative;
  height: 600px;
  overflow-y: auto;
  border: 1px solid #ebeef5;
}
​
.el-icon-arrow-right {
  transition: transform 0.2s;
  margin-right: 8px;
  cursor: pointer;
}
​
.el-icon-arrow-right.expanded {
  transform: rotate(90deg);
}
​
/* 隐藏空元素的边框 */
.el-table__row:last-child td {
  border-bottom: none !important;
}
</style>