需求:做一个报表,支持横向滚动,数据量较大(大概几百到几千条数据)
方案
使用vxe-table ui实现
遇到的困难
由于数据的表头是动态计算的二级表头,并且数据量大,导致滚动起来十分卡顿,不流畅
卡顿原因
解决方案
代码实现
<template>
<div
v-loading="loading"
class="virtual-table-wrap relative flex-1 flex flex-col bg-#fff"
>
<span v-if="dataList.length > 15" class="white_model"></span>
<vxe-grid
ref="xGridHeader"
class="table-header"
v-bind="headerOptions"
:class="dataList.length > 15 ? 'mr20px' : ''"
/>
<vxe-grid
ref="xGridBody"
class="table-body"
v-bind="bodyOptions"
:footer-method="footerMethod"
:merge-footer-items="[{ row: 0, col: 0, rowspan: 1, colspan }]"
v-on="gridEvents"
@scroll="onScroll"
>
<template v-for="(_, name) in $slots" #[name]="scoped">
<slot :name="name" v-bind="scoped || {}"></slot>
</template>
</vxe-grid>
</div>
</template>
<script setup lang="ts">
import { fenToRMBStr } from '@hg/utils/format'
/****
*
* 基于vxe-table的虚拟滚动表格,多表头滚动
*
* ***/
const emits = defineEmits(['handCellClick'])
// 最多支持横向2w列,纵向5w行数据虚拟滚动
// 必须要固定列宽
const props = defineProps({
/* 合并的列数,(坐标为0,,0) */
colspan: {
type: Number,
default: 3,
},
/* 一次加载列数(不算右侧固定列) */
pageSize: {
type: Number,
default: 100,
},
/* 右侧固定的列数 */
rightFixedNumber: {
type: Number,
default: 3,
},
/* 距离右侧距离加载下一页 */
loadDistanceToRight: {
type: Number,
default: 100,
},
})
const loading = ref(false)
const headerOptions = computed(() => {
return {
border: true,
resizable: false,
height: 94,
align: 'left',
'show-overflow': true,
'show-header-overflow': true,
}
})
const bodyOptions = computed(() => {
return {
data: [],
border: true,
resizable: false,
height: 800,
align: 'left',
showFooter: true,
'show-overflow': true,
'show-header-overflow': true,
loading: loading.value,
'show-header': false,
'scroll-x': {
enabled: true,
gt: 0,
},
'scroll-y': {
enabled: true,
gt: 0,
},
}
})
const columns = ref([]) // 所有列
const dataList = ref([]) // 所有data
const page = ref(1)
// 计算表尾数据
function footerMethod({ columns, data }: any) {
return [
columns.map((column: any, columnIndex: number) => {
// console.log('合计', column)
if (column.params?.noCompute) {
return '-'
}
if (columnIndex === 0) {
return '合计'
}
// 可发货余额
if (column.field === 'deliveryTotal') {
return fenToRMBStr(
sumNum(data, 'creditBalance') * 1 + sumNum(data, 'balanceEnd') * 1
)
}
if (column.params?.isMoney) {
return fenToRMBStr(sumNum(data, column.field))
}
if (column.field.indexOf('number') > -1) {
return sumNum(data, column.field)
}
}),
]
}
function sumNum(list: any[], field: string) {
let count = 0
list.forEach(item => {
count += isNaN(Number(item[field])) ? 0 : Number(item[field])
})
return count
}
function onScroll({ isX, $event, scrollLeft }: any) {
if (isX) {
const $scrollTarget = $event.target
xGridHeader.value.scrollTo(scrollLeft)
if (isScrolledToRight($scrollTarget)) {
queryMore()
}
}
}
/* 滚动到右侧 */
function isScrolledToRight(element: Element, diff = 5) {
return (
element.scrollLeft + element.clientWidth >=
element.scrollWidth - diff - props.loadDistanceToRight
)
}
/* 初始化数据 */
function initData(colList: any, list: any) {
columns.value = colList
dataList.value = list
if (columns.value.length < props.pageSize) {
loadData(columns.value)
} else {
loadColumns()
}
}
async function queryMore() {
page.value++
loadColumns()
}
const xGridHeader = ref()
const xGridBody = ref()
/* 处理列数据 */
const finished = ref(false) // 是否全部加载完成
async function loadColumns() {
let rightItemList = []
let leftItemList = []
let allItemList = []
rightItemList = columns.value.slice(
columns.value.length - props.rightFixedNumber
)
// debugger
/* 截取 */
const size = page.value * props.pageSize
// debugger
if (columns.value.length < props.pageSize) {
leftItemList = columns.value
} else {
leftItemList = columns.value.slice(
0,
size > columns.value.length ? columns.value.length - 1 : size
)
}
allItemList = [...leftItemList, ...rightItemList]
// 加载下一页
if (!finished.value && allItemList.length < columns.value.length + 1) {
loadData(allItemList)
finished.value = allItemList.length === columns.value.length
}
}
/* 数据加载 */
function loadData(list: any) {
loading.value = true
xGridHeader.value?.reloadColumn(list)
xGridBody.value?.reloadColumn(flatArray(list as any))
xGridBody.value?.reloadData(dataList.value)
loading.value = false
}
// 取数组的children,
function flatArray(list: [], result = []): any[] {
return list.reduce((acc: any, cur: any) => {
if (cur.children && cur.children.length > 0) {
return flatArray(cur.children, acc)
}
acc.push(cur)
return acc
}, result)
}
/* 表格事件 */
const gridEvents = {
cellClick(params: any) {
emits('handCellClick', params)
},
}
defineExpose({ initData })
</script>
<style lang="scss">
.virtual-table-wrap {
display: flex;
flex-direction: column;
padding: 22px 40px 40px !important;
::-webkit-scrollbar {
width: 20px;
}
::-webkit-scrollbar-thumb {
background-color: #b6b6b6;
}
.table-header {
position: relative;
// margin-right: 20px;
/* 隐藏滚动条,启用滚动 */
.vxe-table--body-wrapper.body--wrapper {
overflow-x: scroll; /* 或者 overflow: auto */
height: 0 !important;
min-height: 0 !important;
padding: 0;
}
/* 针对 WebKit 浏览器隐藏滚动条 */
.vxe-table--body-wrapper.body--wrapper::-webkit-scrollbar {
display: none;
}
.vxe-table--body-wrapper {
height: 0 !important;
}
.vxe-table--empty-placeholder {
display: none !important;
}
.vxe-table--empty-block {
height: 0px;
}
}
}
</style>
<style scoped lang="scss">
.white_model {
position: absolute;
width: 19px;
height: 95px;
background-color: #f8f8f9;
right: 40px;
top: 22px;
border: 1px solid #e8eaec;
border-left: none;
}
</style>
效果