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>