Element UI Table组件对某行上下移动,并且移动行保持在可视区域内

2,082 阅读4分钟

需求分析

今天接到个需求,对列表数据进行上下移动,实现排序效果,并且移动数据时,要保持移动的数据在可视区域中。

  1. 选中行上下移动,可以操作dom(不推荐,毕竟vue是以数据驱动的),或者对数据进行操作
  2. 选中行保持在可视范围内(要让元素保持在可视范围内,有很多方法实现,我这种场景是设置容器的scrollTop,让容器滚动,以达到我们的目的,后面会解释)

第一步:基础代码

html代码:

<el-table
  ref="deptSort"
  :data="list"
  @row-click="handleClick"
>
...
</el-table>

通过el-tablerow-click方法获取选中的那一行:

第二步:移动数据的方式

需求分析1,有说到两种移动数据的方式:

方式一: 对dom进行操作实现上下移动,不是说不可以。既然我们用vue开发项目,能操作数据实现的,我们就尽量不操作dom,当然有些场景,不得不操作dom。

方式二: 操作数据。

首先获取选中行的下标

handleClick(row, col, event) {
  let currIndex = ''
  currIndex = this.list.findIndex(e => {
    return e.exam_dept_id == row.exam_dept_id
  })
  this.$set(this.currIndex, 'currIndex', currIndex); //获取选中行的下标
  this.$set(this.currEvent, 'currEvent', event);//保存点击的event,后面用到
}

通过选中行的下标向上移动数据

handleMoveUp() {
  let currIndex = this.currIndex,
  list = this.list
  if (currIndex) {
    let currRow = list[currIndex] //当前行
    let temp = JSON.parse(JSON.stringify(list[currIndex - 1])) //上一行
    this.$set(list, currIndex - 1, currRow)
    this.$set(list, currIndex, temp)
    this.currIndex = currIndex - 1 //此处是为了保存选中行移动后的下标
  } else if (currIndex == 0) {
    this.$message({
      message: '排序第一的无法上移',
      type: 'error'
    })
  } else {
    this.$message({
      message: '请选中后操作',
      type: 'error'
    })
  }
}

向下移动类似操作。

第三步:让移动的数据保持在可视区域中

首先,选中行保持在可视区域中,不是让他脱离文档流,固定在头部,而是在移动时,若超出可视范围了,整个列表上移或者下移,所以不能用position:fixed 或者 position:absolute

然后,如何知道选中行是否在科室范围呢?

  1. 数据上移后,判断选中行dom的顶部是否高于可视区域dom的顶部:通过选中行的offsetTop和滚动容器的scrollTop进行对比,若offsetTop<scrollTop,表示选中行已离开可视区域
  2. 数据下移后,判断选中行dom的底部值currTrFloor是否低于可视区域dom的底部值wrapFloor:若currTrFloor>wrapFloor,表示选中行已离开可视区域

代码如下:

scrollTable(type) { // type为移动方式
  let currIndex = this.currIndex, // 当前选中tr的下标
  tableDom = this.currEvent.target.parentNode.parentNode.parentNode, //当前滚动tr的父元素table DOM元素
  currTrDom = tableDom.querySelector('.current-row'), //当前滚动的tr DOM元素
  wrapDom = tableDom.parentNode.parentNode, //当前列表容器的DOM元素
  wrapScrollTop = wrapDom.scrollTop, //容器滚动高度
  trOffsetTop = currTrDom.offsetTop//当前tr距离顶部距离

  if (type == 'up') { // 上移
    if (wrapScrollTop > trOffsetTop) { //tr顶部超出可视区域顶部
      wrapDom.scrollTop = trOffsetTop
    }
  } else { // 下移
    let currTrFloor = trOffsetTop + currTrDom.offsetHeight,//当前tr底部值
      wrapFloor = wrapScrollTop + wrapDom.clientHeight//可视区域底部值
    if (currTrFloor > wrapFloor) {//tr底部超出可视区域底部
      wrapDom.scrollTop = wrapScrollTop + (currTrFloor - wrapFloor)
    }
  }
}

到此,基本的逻辑代码已经实现,将第三步代码添加到第二步代码中,代码如下:

//数据移动后,判断是否选中行在可视区域中
setTimeout(() => {
  this.scrollTable('up')
},20)

上面的setTimeout是为了数据移动后,确保判断逻辑在页面重新渲染之后执行,作用与vue的$nextTick相同,但是$nextTick偶尔有坑,所以不用。20ms后执行,是因为数据改变,触发视图更新的时间大概为17ms,经过测试所得。

既然加了定时器,习惯性的会考虑节流或者防抖,这个场景适用节流,最终js代码如下:

// 选中当前行
handleClick(row, col, event) {
  let currIndex = ''
  currIndex = this.list.findIndex(e => {
    return e.exam_dept_id == row.exam_dept_id
  })
  this.currIndex = currIndex //保存选中行的下标
  this.$set(this.currEvent, 'currEvent', event);//保存点击的event,后面用到
},
//向上移动
handleMoveUp() {
  if (this.isScrolling) { //节流
    return
  }
  this.isScrolling = true
  let currIndex = this.currIndex,
  list = this.list
  if (currIndex) {
    let currRow = list[currIndex] //当前行
    let temp = JSON.parse(JSON.stringify(list[currIndex - 1])) //上一行
    this.$set(list, currIndex - 1, currRow)
    this.$set(list, currIndex, temp)
    this.currIndex = currIndex - 1 //此处是为了保存选中行移动后的下标
    
    //数据移动后,判断选中行是否在可视区域中
    setTimeout(() => {
      this.scrollTable('up')
    },20)
    
  } else if (currIndex == 0) {
    this.isScrolling = false
    this.$message({
      message: '排序第一的无法上移',
      type: 'error'
    })
  } else {
    this.isScrolling = false
    this.$message({
      message: '请选中后操作',
      type: 'error'
    })
  }
},
// 判断选中行是否在可视区域中
scrollTable(type) { // type为移动方式
  let currIndex = this.currIndex, // 当前选中tr的下标
  tableDom = this.currEvent.target.parentNode.parentNode.parentNode, //当前滚动tr的父元素table DOM元素
  currTrDom = tableDom.querySelector('.current-row'), //当前滚动的tr DOM元素
  wrapDom = tableDom.parentNode.parentNode, //当前列表容器的DOM元素
  wrapScrollTop = wrapDom.scrollTop, //容器滚动高度
  trOffsetTop = currTrDom.offsetTop//当前tr距离顶部距离

  if (type == 'up') { // 上移
    if (wrapScrollTop > trOffsetTop) { //tr顶部超出可视区域顶部
      wrapDom.scrollTop = trOffsetTop
    }
  } else { // 下移
    let currTrFloor = trOffsetTop + currTrDom.offsetHeight,//当前tr底部值
      wrapFloor = wrapScrollTop + wrapDom.clientHeight//可视区域底部值
    if (currTrFloor > wrapFloor) {//tr底部超出可视区域底部
      wrapDom.scrollTop = wrapScrollTop + (currTrFloor - wrapFloor)
    }
  }
  this.isScrolling = false
}

以上是我实现此功能的全部代码,有不足之处,欢迎指出,谢谢