实现 element-plus 表格多选时按 shift 进行连选的功能
前言
element-plus表格提供了多选功能,可单击勾选一条数据,可全选。
现在有个很合理的需求,希望实现类似于文件系统中shift连续选择功能,并且在表格排序后,依照排序后的顺序连选。
在线演示
在线演示 (示例基于 element-plus@2.2.9, vue@3.2.37)
一、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>
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
}
}
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
// }
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
}
}
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