实现 element-plus 表格多选时按 shift 进行连选的功能
前言
element-plus表格提供了多选功能,可单击勾选一条数据,可全选。
现在有个很合理的需求,希望实现类似于文件系统中shift连续选择功能,并且在表格排序后,依照排序后的顺序连选。
一、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 多选表格示例
<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:
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 = idxreturn 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') returnif(shiftFlag.value) returnshiftFlag.value = true}function handleKeyUp({ key }) {if(key !== 'Shift') returnif(!shiftFlag.value) returnshiftFlag.value = falselastIdx.value = -1}// el-table@selectfunction handleTbSelect(selection, row) {updateSelection(selection)// 若未按 shift,更新 lastIdxif(!shiftFlag.value) {if(selection.find(r => r.index === row.index)) lastIdx.value = row.indexreturn}// 若 lastIdx 无有效值,记录本次下标if(lastIdx.value === -1) {lastIdx.value = row.indexreturn}// 若 lastIdx 有值,自动勾选中间rowslet [start, end] = [lastIdx.value, row.index]if(start > end) [start, end] = [end, start]// TODO: toggleRowSelection// ...}// el-table@selection-changefunction handleTbSelectionChange(selection) {updateSelection(selection)}// 更新 selectedRowsfunction updateSelection(selection) {selectedRows.value = selection}return {selectedRows: readonly(selectedRows),handleTbSelect,handleTbSelectionChange}
}
组件中引入使用
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
// }
3、小结
注意:在下面的完整代码中,本人监听了 el-table 组件内部状态存储信息(tableEl.store.states.data
),未公布在官网。随着版本的更新,有失效的风险。建议锁定 element-plus 版本号或者确定所使用的版本仍有效。
不监听表格绑定的数据本身,是因为el-table排序后,不会体现在绑定的数据上,也就是说,排序后,连选功能还是按照排序前的顺序来进行连选的。
三、代码与演示
在线演示
工具方法完整代码:
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 = newValtbFlag.value = true}}, { deep: true, immediate: true })onMounted(() => {mountedFlag.value = trueif(!isSetup) {tbFlag.value = truetableEl = 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') returnif(shiftFlag.value) returnshiftFlag.value = true}function handleKeyUp({ key }) {if(key !== 'Shift') returnif(!shiftFlag.value) returnshiftFlag.value = falselastIdx.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 = idxreturn 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.indexreturn}// lastIdx为-1时,记录本次下标if(lastIdx.value === -1) {lastIdx.value = row.indexreturn}// lastIdx 有值,自动勾选中间rowslet [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)) continueif(!checkRowSelectable1(tmp)) continuetemp.push(tmp)}onprogress.value = truefor(let i = 0, len = temp.length; i < len; i++) {if(i === len - 1) onprogress.value = falsetableEl.toggleRowSelection(temp[i], true)}})}// 更新 selectedRowsfunction updateTbSelection(selection) {selectedRows.value = selection}// 清空 selectedRowsfunction clearSelection() {selectedRows.value = []}function checkRowSelectable1(row, rowIndex) {return typeof(checkRowSelectable) === 'function'? checkRowSelectable(row, rowIndex): true}return {selectedRows: readonly(selectedRows),handleTbSelect,handleTbSelectionChange,clearSelection}
}