当前位置:   article > 正文

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

element-plus表格实现按shift键快捷多选

前言

element-plus表格提供了多选功能,可单击勾选一条数据,可全选。
现在有个很合理的需求,希望实现类似于文件系统中shift连续选择功能,并且在表格排序后,依照排序后的顺序连选。

在线演示 (示例基于 element-plus@2.2.9, vue@3.2.37)


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

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

  1. 插入多选项

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

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

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

2、el-table 多选表格示例

<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
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

二、实现

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

1、分析

基本过程:

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

细节依据需求调整

2、实现

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

el-table-multi-select.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
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80

组件中引入使用
demo.vue:

<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
// }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40

3、小结

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

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

三、代码与演示

在线演示

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

工具方法完整代码:

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
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Monodyee/article/detail/114552
推荐阅读
相关标签
  

闽ICP备14008679号