vue3 + elementplus 手动实现 el-table 虚拟滚动

<!-- 动态渲染右侧表格 -->
        <el-table :data="rightTableData" style="width: 100%" border size="small" 
          ref="rightTableRef" show-summary>
          <el-table-column :prop="prop" :label="label" v-for="item in tableHeader" :key="item.prop"
            align="center" :width="item.width">
            <template #default="scope">
              <!-- 动态可见区域判断 -->
              <template v-if="scope.$index >= visibleRange.start && scope.$index <= visibleRange.end">
              <!-- 根据编辑状态显示不同的内容 -->
	              <template v-if="isEditing(scope.$index)">
	                <span v-if="item.type == 'noraml'">{{ scope.row[scope.column.property] }}</span>
	                <el-input size="small" v-model.lazy="scope.row[scope.column.property]"></el-input>
	              </template>
              </template>
              <!-- 非可见区域显示简单文本 -->
              <template v-else>
                <!-- <span>{{ scope.row[scope.column.property] }}</span> -->
                <div style="height: 33px; background-color: #f0f0f0;"></div>
              </template>
            </template>
          </el-table-column>
        </el-table>

<script lang="ts" setup>
import { ref, Ref, reactive, onMounted, markRaw, onBeforeUnmount, nextTick, watch } from "vue";

const rightTableRef: any = ref(null)
const visibleRange = reactive({ start: 0, end: 0 })
const rowHeights = reactive<{ [key: number]: number }>({})
const offsets = reactive<number[]>([])
const currentEditingIndex: any = ref(null) // 当前正在编辑的行索引

const tableHeader = ref([
  { label: "表头1", prop: "", type: "noraml", width: '120' },
  { label: "表头2", prop: "", type: "input", width: '90' },
]}
onMounted(async () => {
  initElTable();
});

const initElTable = () => {
  cleanupTable()
  // 延迟绑定滚动事件(确保表格渲染完成)
  setTimeout(() => {
    const scrollBody = rightTableRef.value?.$el.querySelector('.el-scrollbar__wrap')
    if (scrollBody) {
      scrollBody.addEventListener('scroll', handleScroll)
      console.log('滚动事件已绑定')
    } else {
      console.error('未找到滚动容器')
    }

    // 首次测量行高
    measureRowHeights()
    // 强制触发一次范围计算
    handleScroll()
  }, 300)
}

// 清理表格逻辑
const cleanupTable = () => {
  const scrollBody = rightTableRef.value?.$el.querySelector('.el-scrollbar__wrap')
  if (scrollBody) {
    scrollBody.removeEventListener('scroll', handleScroll)
    console.log('滚动事件已解绑')
  }
}

// 关键修复1:精确获取行元素
const getTableRows = () => {
  return rightTableRef.value?.$el.querySelectorAll('.el-table__body tbody tr') as NodeListOf<HTMLElement>
}

// 关键修复2:带重试和高度缓存的行高测量
const measureRowHeights = async (retry = 3) => {
  await nextTick()
  const rows = getTableRows()

  if (!rows?.length && retry > 0) {
    setTimeout(() => measureRowHeights(retry - 1), 300)
    return
  }

  rows.forEach((row, index) => {
    const height = row.offsetHeight
    if (rowHeights[index] !== height) {
      rowHeights[index] = height
    }
  })

  rebuildOffsets()
}

// 关键修复3:正确构建累积高度数组
const rebuildOffsets = () => {
  const newOffsets = [0]
  let total = 0
  for (let i = 0; i < Object.keys(rowHeights).length; i++) {
    total += rowHeights[i] || 0
    newOffsets.push(total)
  }
  offsets.splice(0, offsets.length, ...newOffsets)
}

// 关键修复4:精确的可见范围计算
const calculateVisibleRange = (scrollTop: number, clientHeight: number) => {
  if (offsets.length <= 1) return { start: 0, end: 0 }
  let start = 0
  let end = offsets.length - 1
  const target = scrollTop + 1 // 避免边界问题

  // 二分查找起始位置
  while (start <= end) {
    const mid = Math.floor((start + end) / 2)
    if (offsets[mid] < target) {
      start = mid + 1
    } else {
      end = mid - 1
    }
  }
  const visibleStart = Math.max(0, start - 1)

  // 线性查找结束位置
  let visibleEnd = visibleStart
  const maxScroll = scrollTop + clientHeight
  while (visibleEnd < offsets.length && offsets[visibleEnd] < maxScroll) {
    visibleEnd++
  }

  // 设置缓冲区域
  // visibleRange.start = Math.max(0, visibleStart - 2)
  // visibleRange.end = Math.min(offsets.length - 2, visibleEnd + 2) // 因为offsets长度比行数大1
  visibleRange.start = Math.max(0, visibleStart)
  visibleRange.end = Math.min(offsets.length - 2, visibleEnd) // 因为offsets长度比行数大1

  console.log('当前滚动位置:', scrollTop)
  console.log('可视区域范围:', visibleRange.start, visibleRange.end)
  // console.log('行高数据:', rowHeights)
  // console.log('累积高度:', offsets)
}

// 关键修复5:可靠的滚动处理
const handleScroll = () => {
  debounce(() => {
    const scrollBody = rightTableRef.value?.$el.querySelector('.el-scrollbar__wrap') as HTMLElement
    if (!scrollBody || offsets.length === 0) return
    calculateVisibleRange(scrollBody.scrollTop, scrollBody.clientHeight)
  }, 300)
}

onBeforeUnmount(() => {
  const scrollBody = rightTableRef.value?.$el.querySelector('.el-scrollbar__wrap')
  scrollBody?.removeEventListener('scroll', handleScroll)
})

const handleRowClick = (row: any) => {
  const rowIndex = rightTableData.value.indexOf(row);
  // 如果点击的是同一行,则不进行任何操作
  if (currentEditingIndex.value === rowIndex) return;
  // 取消之前的编辑状态
  currentEditingIndex.value = null;
  // 设置新的编辑状态
  nextTick(() => {
    currentEditingIndex.value = rowIndex;
  });
}
const isEditing = (index: any) => {
  return index === currentEditingIndex.value;
}
</script>

你可能感兴趣的:(vue.js,javascript,前端)