如何实现Element Table的拖拽框选行数据,一个可以拿来即用的vuejs的mixins拖拽工具。

1,240 阅读3分钟

如何实现Element Table的拖拽框选行数据。一个可以即用的mixins拖拽工具。

最近项目上,产品有一个需求,用户想拖拽去勾选一行或多行数据。比起常见的点击勾选,拖拽勾选的灵活性、操作性更好,对用户更友好。

那我们现在来实现一个,大体思路:

  • 首先梳理实现思路。我们要实现第一步拖动,需要监听鼠标的mousedown事件和mouseup事件,以及mouseover事件;

  • 在mousedown事件开始时,我们要获取点击的位置信息,作为框选起点;同样的把mouseup事件结束时的位置作为结束位置;

  • 有了这两个位置,我们就可以得到一个框选的坐标范围,通过对框选范围与表格checkbox元素的相交和包含判断,来计算得到框选了哪几行的数据;

  • 但是对于用户来说,框选的位置需要实时显示,这样用户才能知道自己的操作。这时候我们可以通过mouseover事件实时计算每个时刻点鼠标所在位置,结合起始位置,就可以得到框选的范围,然后我们将范围画出来。

有了思路,我们就可以动手编写代码了,这里我采用canvas来画出矩形。

import { vwToPx } from '@/utils/index'
/**
 * 表格拖拽勾选mixins
 * 注意事项:
 * 1、表格的data属性为tableData
 * 2、表格的ref为tableRef,如果没有固定的可以设置 currentTableRef
 * 3、页面根节点的ref为pageRef(非必选,可省略),pageRef必须作用在原生标签上
 *
 * 4、页面/组件的data属性不能覆盖mixins中除noReverseSelect以外的其他字段 -- 防止出现不可预知的问题(特别重要)
 */
export default {
  data() {
    return {
      selecting: false,
      startX: 0,
      startY: 0,
      endX: 0,
      endY: 0,
      canvas: null,
      ctx: null,
      selectionColumnRect: null,
      canvasOffsetTop: 0,
      canvasOffsetLeft: 0,
      tableElDom: null,
      isCheckboxFixed: false, // 是否fixed布局
      clearSelectionTimer: void 0, // 定时器,解决点击勾选和拖拽勾选的问题

      // 是否不能反选,可以在页面自行替换,默认true(不能反选)
      noReverseSelect: true
    }
  },
  mounted() {
    window.addEventListener('mousedown', this.onMouseDown)
    window.addEventListener('mousemove', this.onMouseMove)
    window.addEventListener('mouseup', this.onMouseUp)
    window.addEventListener('resize', this.updateCanvasSize)
  },
  methods: {
    createCanvas() {
      this.canvas = document.createElement('canvas')
      this.canvas.style.position = 'absolute'
      this.canvas.style.zIndex = 100
      document.body.appendChild(this.canvas)
      this.ctx = this.canvas.getContext('2d')
      this.updateCanvasSize()
    },
    removeCanvas() {
      if (this.canvas) {
        document.body.removeChild(this.canvas)
        this.canvas = null
        this.ctx = null
      }
    },
    updateCanvasSize() {
      if (!this.canvas) return
      // 如果表格勾选列时fixed时
      let app = this.$refs.pageRef
      if (!app) {
        app = document.querySelector('.app-main').firstChild || {}
      }
      this.canvas.width = app.offsetWidth
      this.canvas.height = app.offsetHeight

      if (this.isCheckboxFixed) {
        const fixedCheckedColumn = this.tableElDom.querySelector('.el-table__fixed')
        const rect = fixedCheckedColumn.getBoundingClientRect()
        this.canvas.style.top = `${rect.top}px`
        this.canvas.style.left = `${rect.left}px`
        this.canvasOffsetTop = rect.top
        this.canvasOffsetLeft = rect.left
      } else {
        // this.canvas.width = app.offsetWidth
        // this.canvas.height = app.offsetHeight
        this.canvas.style.top = `${app.offsetTop}px`
        this.canvas.style.left = `${app.offsetLeft}px`
        this.canvasOffsetTop = app.offsetTop
        this.canvasOffsetLeft = app.offsetLeft
      }
      // }
    },
    /**
     * 判断鼠标点击区域是否在第一列,且为selection列
     */
    getSelectionColumnRect() {
      let table = null
      if (this.isCheckboxFixed) {
        table = this.tableElDom.querySelector('.el-table__fixed-body-wrapper tbody')
      } else {
        table = this.tableElDom.querySelector('.el-table__body-wrapper tbody')
      }
      const firstRow = table.querySelector('tr')
      if (firstRow) {
        const selectionCell = firstRow.querySelector('.el-table-column--selection')
        if (selectionCell) {
          this.selectionColumnRect = selectionCell.getBoundingClientRect()
        }
      }
    },
    /**
     * 开始拖动
     * @param {Event} event
     */
    startTableDrag(event) {
      this.selecting = true
      this.startX = event.clientX
      this.startY = event.clientY
      this.endX = this.startX
      this.endY = this.startY
      this.getSelectionColumnRect()
      this.createCanvas()
    },

    onMouseDown(event) {
      // 限制必须是左键点击
      if (event.button !== 0) {
        // event.preventDefault()
        return
      }
      const _tableRef = this.$refs.tableRef || this.$refs[this.currentTableRef]
      if (!_tableRef) return
      this.tableElDom = _tableRef.$el

      // 判断是否checkbox是fixed布局
      this.isCheckboxFixed = !!(this.tableElDom && this.tableElDom.querySelector('.el-table__fixed .el-checkbox'))
      // 限制在selection列,但是不能覆盖el-checkbox,因为el-checkbox需要能手动点击
      // if (event.target.closest('.el-table') && event.target.closest('.el-table-column--selection') && !event.target.closest('.el-checkbox')) {

      // 解决框选和点击的区分,并限制为tbody里面的节点
      if (event.target.closest('.el-table') && event.target.closest('tbody .el-table-column--selection')) {
        if (event.target.closest('.el-checkbox')) {
          this.clearSelectionTimer = setTimeout(() => {
            this.startTableDrag(event)
          }, 200)
        } else {
          this.startTableDrag(event)
        }
      }
    },
    onMouseMove(event) {
      if (!this.selecting) return
      // 限制框选在selection列
      // this.endX = Math.min(Math.max(event.clientX, this.selectionColumnRect?.left), this.selectionColumnRect?.right)
      this.endX = event.clientX
      // this.endY = event.clientY

      const tableBody = this.tableElDom.querySelector('.el-table__body-wrapper')
      // // 限制在表格内
      const tableRect = tableBody ? tableBody.getBoundingClientRect() : {}

      // // 判断是否有滚动条
      const hasScrollbar = tableBody && tableBody.scrollHeight > tableBody.clientHeight
      let bar_width = 0
      if (hasScrollbar) {
        // 去掉滚动条高度的常量
        const BAR_WIDTH_VW = (10 / 1920) * 100 // 以1920宽度设置默认值
        bar_width = vwToPx(BAR_WIDTH_VW) // 根据屏幕实际宽度,计算滚动条宽度
      }

      // 添加BAR_WIDTH,为了解决fixed布局时框选数量不一致的问题
      this.endY = Math.min(Math.max(event.clientY, tableRect.top), tableRect.bottom) - bar_width
      this.drawSelection()
    },
    onMouseUp() {
      if (this.clearSelectionTimer) {
        clearTimeout(this.clearSelectionTimer)
        this.clearSelectionTimer = void 0
      }
      if (!this.selecting) return
      this.selecting = false
      this.selectTableDragRows()
      this.clearSelection()
      this.removeCanvas()
    },
    drawSelection() {
      this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
      this.ctx.fillStyle = 'rgba(28, 122, 244, 0.3)'
      const rectWidth = this.endX - this.startX
      const rectHeight = this.endY - this.startY
      this.ctx.fillRect(this.startX - this.canvasOffsetLeft, this.startY - this.canvasOffsetTop, rectWidth, rectHeight)

      // 添加边框,并设置边框颜色
      this.ctx.lineWidth = 1
      this.ctx.strokeStyle = 'rgba(28, 122, 244, 1)'
      this.ctx.strokeRect(this.startX - this.canvasOffsetLeft, this.startY - this.canvasOffsetTop, rectWidth, rectHeight)
    },
    clearSelection() {
      if (this.ctx) {
        this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
      }
    },
    /**
     * 拖拽完成,计算并触发checkbox勾选
     */
    selectTableDragRows() {
      let table = null
      if (this.isCheckboxFixed) {
        table = this.tableElDom.querySelector('.el-table__fixed-body-wrapper tbody')
      } else {
        table = this.tableElDom.querySelector('.el-table__body-wrapper tbody')
      }
      // 是否交界,限制在div.cell,如需为行,则可以只保留tr
      const rows = table.querySelectorAll('tr .el-table-column--selection .cell')
      const startX = Math.min(this.startX, this.endX)
      const startY = Math.min(this.startY, this.endY)
      const endX = Math.max(this.startX, this.endX)
      const endY = Math.max(this.startY, this.endY)
      rows.forEach((row, index) => {
        const rect = row.getBoundingClientRect()
        const withinSelection = rect.top < endY && rect.bottom > startY && rect.left < endX && rect.right > startX
        if (withinSelection) {
          const checkbox = row.querySelector('.el-checkbox .el-checkbox__input')
          // 不能反选
          if (this.noReverseSelect) {
            // 1、触发手动勾选checkbox,且没有勾选的
            if (checkbox && !checkbox.getAttribute('class').includes('is-checked')) {
              checkbox.click()
            }
            // 2、使用表格来勾选
            // const _tableRef = this.$refs.tableRef || this.$refs[this.currentTableRef]
            // _tableRef.toggleRowSelection(this.tableData[index], true)
          } else if (checkbox) {
            // 可以反选
            checkbox.click()
          }
        }
      })
    }
  },
  beforeDestroy() {
    window.removeEventListener('mousedown', this.onMouseDown)
    window.removeEventListener('mousemove', this.onMouseMove)
    window.removeEventListener('mouseup', this.onMouseUp)
    window.removeEventListener('resize', this.updateCanvasSize)
    this.removeCanvas()
  }
}
 

这样,就可以实现表格拖拽效果。效果如下:

image.png

最后这里页面使用了vw来做多端适配,引用了vwToPx工具方法

/**
 * 将vw单位转换为px单位
 * @param {number} vw 需要转换的vw值
 * @return {number} 转换后的px值
 */
export function vwToPx(vw) {
  var viewportWidth = window.innerWidth // 获取视口宽度
  return Math.ceil((vw / 100) * viewportWidth)
}