实现 element-plus 表格多选时按 shift 进行连选的功能

前言

element-plus表格提供了多选功能,可单击勾选一条数据,可全选。

现在有个很合理的需求,希望实现类似于文件系统中shift连续选择功能,并且在表格排序后,依照排序后的顺序连选。

在线演示

在线演示 (示例基于 [email protected], [email protected]

一、el-table 多选表格基本使用

1. el-table 相关事件、方法

  • 插入多选项

<el-table-column type="selection" />

  • 表格事件
事件名说明回调参数
select当用户手动勾选数据行的 Checkbox 时触发的事件selection, row
select-all当用户手动勾选全选 Checkbox 时触发的事件selection
selection-change当选择项发生变化时会触发该事件selection
  • 表格方法
方法名说明参数
clearSelection用于多选表格,清空用户的选择-
getSelectionRows返回当前选中的行
toggleRowSelection用于多选表格,切换某一行的选中状态, 如果使用了第二个参数,则可直接设置这一行选中与否row, selected
toggleAllSelection用于多选表格,切换全选和全不选-
  • table-column 属性

selectable: 仅对 type=selection 的列有效,类型为 Function,Function 的返回值用来决定这一行的 CheckBox 是否可以勾选。(类型:function(row, index)

2. el-table 多选表格示例

html
<template>
  <el-table
    :data="tableData"
    @selection-change="handleSelectionChange"
  >
    <el-table-column type="selection" />
    <!-- other columns -->
  </el-table>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { getTableData } from '@/api/demo.js'

const tableData = ref([])
const selectedRows = ref([])

onMounted(() => {
  getData()
})

const getData = () => {
  getTableData().then(res => {
    tableData.value = res.data ?? []
  })
}

const handleSelectionChange = (selection) => {
  selectedRows.value = selection
}
</script>

二、实现

由于该功能可能会应用到很多页面中,最好是提供一个统一的公共方法,在页面中引入使用

1. 分析

基本过程:

> 记录上一次点击的数据;记录 shift 状态 > 组件挂载后,监听 shift 键的 keydown, keyup 事件,更新 shift 状态;组件销毁前取消相关监听 > 监听 el-table@select 事件,对比上一次点击与本次点击,计算待连选数据列表后,使用 toggleRowSelection 方法进行连续勾选

细节依据需求调整

2. 实现

根据分析,初步创建方法 ==注意==: 为了方便理解思路,下面的示例中仅包含关键代码,完整代码在最后

el-table-multi-select.js:

js
import { ref, readonly, watch, onMounted, onBeforeUnmount } from 'vue'

export function elTableMultiSelect(tableEl) {
  // 表格数据
  const tableData = ref([])
  // 选中数据列表
  const selectedRows = ref([])
  // 下标记录
  const lastIdx = ref(-1)
  // shift标识
  const shiftFlag = ref(false)
  
  onMounted(() => {
    // el-table 表格排序不会体现在绑定的数据列表上
    // 监听 el-table 组件内部状态存储中的表格数据(避免表格排序后连选不连续的bug)
    watch(tableEl.store?.states?.data, (newVal) => {
      tableData.value = newVal.map((item,idx) => {
        item._index = idx
        return item
      })
    }, { immediate: true })
      
    // Shift监听/取消监听
    document.addEventListener('keydown', handleKeyDown)
    document.addEventListener('keyup', handleKeyUp)
  })
  // 取消Shift监听
  onBeforeUnmount(() => {
    document.removeEventListener('keydown', handleKeyDown)
    document.removeEventListener('keyup', handleKeyUp)
  })
  // Shift事件处理
  function handleKeyDown({ key }) {
    if(key !== 'Shift') return
    if(shiftFlag.value) return
    shiftFlag.value = true
  }
  function handleKeyUp({ key }) {
    if(key !== 'Shift') return
    if(!shiftFlag.value) return
    shiftFlag.value = false
    lastIdx.value = -1
  }
  // el-table@select
  function handleTbSelect(selection, row) {
    updateSelection(selection)
    
    // 若未按 shift,更新 lastIdx
    if(!shiftFlag.value) {
      if(selection.find(r => r._index === row._index)) lastIdx.value = row._index
      return
    }
    // 若 lastIdx 无有效值,记录本次下标
    if(lastIdx.value === -1) {
      lastIdx.value = row._index
      return
    }

    // 若 lastIdx 有值,自动勾选中间rows
    let [start, end] = [lastIdx.value, row._index]
    if(start > end) [start, end] = [end, start]
    
    // TODO: toggleRowSelection
    // ...
  }
  // el-table@selection-change
  function handleTbSelectionChange(selection) {
    updateSelection(selection)
  }
  // 更新 selectedRows
  function updateSelection(selection) {
    selectedRows.value = selection
  }
  
  return {
    selectedRows: readonly(selectedRows),
    handleTbSelect,
    handleTbSelectionChange
  }
}

组件中引入使用

demo.vue:

html
<template>
  <el-table
    :data="tableData"
    @select="handleTbSelect"
    @selection-change="handleTbSelectionChange"
    ref="demoTable"
  >
    <el-table-column type="selection" />
    <!-- other columns -->
  </el-table>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { getTableData } from '@/api/demo.js'
import { elTableMultiSelect } from '@/use/el-table-multi-select'

const demoTable = ref()
const tableData = ref([])
// const selectedRows = ref([]) // 从公共方法中导出

const {
  selectedRows,
  handleTbSelect,
  handleTbSelectionChange
} = elTableMultiSelect(demoTable)

onMounted(() => {
  getData()
})

const getData = () => {
  getTableData().then(res => {
    tableData.value = res.data ?? []
  })
}

// 从公共方法中导出
// const handleSelectionChange = (selection) => {
//   selectedRows.value = selection
// }

3. 小结

==注意==:在下面的完整代码中,本人监听了 el-table 组件内部状态存储信息(tableEl.store.states.data),未公布在官网。随着版本的更新,有失效的风险。建议锁定 element-plus 版本号或者确定所使用的版本仍有效。

不监听表格绑定的数据本身,是因为el-table排序后,不会体现在绑定的数据上,也就是说,排序后,连选功能还是按照排序前的顺序来进行连选的,连选就毫无意义了。

三、代码与演示

在线演示

完整代码中兼容了 vue3 的两种代码风格,用法在注释中。

工具方法完整代码:

js
import { ref, readonly, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'

/**
 * 工具类: el-table 可多选表格,增加shift连选功能
 * 
 * @param {string|Element} tableRef 选项式API中,传表格ref字符串;setup中,传表格对象
 * @param {Function} [checkRowSelectable] 禁选方法(可选),对应el-table-column selectable属性值
 * @returns {Object} {
 *   selectedRows: 多选结果列表,
 *   handleTbSelect: el-table@select,
 *   handleTbSelectionChange: el-table@selection-change,
 *   clearSelection: 清空多选结果列表
 * }
 * @example
 * // 一、引入
 * 
 * import { elTableMultiSelect } from '@/use/el-table-multi-select'
 * 
 * // 二、template
 * 
 * // el-table 相关属性方法
 *   @select="handleTbSelect"
 *   @selection-change="handleTbSelectionChange"
 *   ref="multiSelectTable"
 * 
 * // 三、方法调用:
 * 
 * ------------------------1. 选项式API:------------------------
 * 
 * // data() 相关变量声明:
 * selectedRows: [],
 * handleTbSelect: undefined,
 * handleTbSelectionChange: undefined
 * 
 * // created() 中解构赋值:
 * ;({
 *   selectedRows: this.selectedRows,
 *   handleTbSelect: this.handleTbSelect,
 *   handleTbSelectionChange: this.handleTbSelectionChange
 * } = elTableMultiSelect.call(this, 'multiSelectTable', this.enableSelection)) // 传表格ref字符串
 * // methods:
 * enableSelection(row, rowIndex) {
 *   return !row.suspected_detection_seq
 * }
 * 
 * ------------------------2. 组合式API:------------------------
 * 
 * const multiSelectTable = ref()
 * const {
 *   selectedRows, handleTbSelect, handleTbSelectionChange
 * } = elTableMultiSelect(multiSelectTable, enableSelection) // 传表格ref对象
 * 
 * function enableSelection(row, rowIndex) {
 *   return !row.suspected_detection_seq
 * }
 */
export function elTableMultiSelect(tableRef, checkRowSelectable) {

  // 表格数据
  const tableData = ref([])
  // 选中数据列表
  const selectedRows = ref([])
  // 下标记录
  const lastIdx = ref(-1)
  // shift标识
  const shiftFlag = ref(false)

  let tableEl                    // 表格对象
  const tbFlag = ref(false)      // 标识:表格挂载完毕
  const mountedFlag = ref(false) // 标识:组件挂载完毕

  const isSetup = typeof(tableRef) !== 'string'
  isSetup && watch(tableRef, (newVal) => {
    if(newVal) {
      tableEl = newVal
      tbFlag.value = true
    }
  }, { deep: true, immediate: true })

  onMounted(() => {
    mountedFlag.value = true
    if(!isSetup) {
      tbFlag.value = true
      tableEl = this.$refs[tableRef]
    }

    // Shift监听/取消监听
    document.addEventListener('keydown', handleKeyDown)
    document.addEventListener('keyup', handleKeyUp)
  })
  // 取消Shift监听
  onBeforeUnmount(() => {
    document.removeEventListener('keydown', handleKeyDown)
    document.removeEventListener('keyup', handleKeyUp)
  })
  // Shift事件处理
  function handleKeyDown({ key }) {
    if(key !== 'Shift') return
    if(shiftFlag.value) return
    shiftFlag.value = true
  }
  function handleKeyUp({ key }) {
    if(key !== 'Shift') return
    if(!shiftFlag.value) return
    shiftFlag.value = false
    lastIdx.value = -1
  }

  // 表格挂载 & 组件挂载 后, 添加 tableData 监听事件
  const tbMountedWatcher = watch([tbFlag, mountedFlag], ([tf, mf]) => {
    if(tf && mf) {
      // el-table 表格排序不会体现在绑定的数据列表上
      // 监听 el-table 组件内部状态存储中的表格数据(避免表格排序后连选不连续的bug)
      watch(tableEl.store?.states?.data, (newVal) => {
        // console.log('watch el-table store', newVal.length)
        tableData.value = newVal.map((item,idx) => {
          item._index = idx
          return item
        })
      }, { immediate: true })
      tbMountedWatcher() // 取消监听
    }
  })

  // toggleRowSelection 会触发 handleTbSelectionChange
  // onprogress 控制shift多选时,只触发一次 handleTbSelectionChange
  // (handleTbSelectionChange 为同步执行方法)
  const onprogress = ref(false)

  /**
   * 当选择项发生变化时会触发该事件
   * @param {Array} selection selected rows
   */
  function handleTbSelectionChange(selection) {
    if(!onprogress.value) updateTbSelection(selection)
  }

  /**
   * 当用户手动勾选数据行的 Checkbox 时触发的事件
   * @param {Array} selection selected rows
   * @param {Object} row table row data
   * @returns 
   */
  function handleTbSelect(selection, row) {
    updateTbSelection(selection)

    if(!shiftFlag.value) {
      if(selection.find(r => r._index === row._index)) lastIdx.value = row._index
      return
    }
    // lastIdx为-1时,记录本次下标
    if(lastIdx.value === -1) {
      lastIdx.value = row._index
      return
    }

    // lastIdx 有值,自动勾选中间rows
    let [start, end] = [lastIdx.value, row._index]
    if(start > end) [start, end] = [end, start]
    nextTick(() => {
      const temp = []
      for(let i = start; i <= end; i++) {
        const tmp = tableData.value[i]
        if(selectedRows.value.find(r => r._index=== tmp._index)) continue
        if(!checkRowSelectable1(tmp)) continue
        temp.push(tmp)
      }
      onprogress.value = true
      for(let i = 0, len = temp.length; i < len; i++) {
        if(i === len - 1) onprogress.value = false
        tableEl.toggleRowSelection(temp[i], true)
      }
    })
  }

  // 更新 selectedRows
  function updateTbSelection(selection) {
    // selectedRows.value = selection
    // keep sequence
    selectedRows.value = selection.slice(0).sort((a, b) => a._index - b._index)
  }

  // 清空 selectedRows
  function clearSelection() {
    selectedRows.value = []
  }

  function checkRowSelectable1(row, rowIndex) {
    return typeof(checkRowSelectable) === 'function'
      ? checkRowSelectable(row, rowIndex)
      : true
  }

  return {
    selectedRows: readonly(selectedRows),
    handleTbSelect,
    handleTbSelectionChange,
    clearSelection
  }
}

Last updated: