【vxe-table实现大量表格数据横向加载滚动】

570 阅读2分钟

需求:做一个报表,支持横向滚动,数据量较大(大概几百到几千条数据)

方案

使用vxe-table ui实现

遇到的困难

由于数据的表头是动态计算的二级表头,并且数据量大,导致滚动起来十分卡顿,不流畅

卡顿原因

image.png

解决方案

image.png

代码实现

<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>

效果

image.png