sone-ui-component-3.2.4 2.1.62 → 2.1.63

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1548 @@
1
+ <template>
2
+ <div
3
+ class="el-table-virtual-scroll"
4
+ :class="[
5
+ isExpanding ? 'is-expanding' : '',
6
+ isHideAppend ? 'hide-append' : '',
7
+ scrollPosition ? `is-scrolling-${scrollPosition}` : '',
8
+ hasFixedRight ? 'has-custom-fixed-right' : '']">
9
+ <slot v-bind="{ headerCellFixedStyle, cellFixedStyle }"></slot>
10
+ </div>
11
+ </template>
12
+
13
+ <script>
14
+ import throttle from 'lodash/throttle'
15
+ import {
16
+ isScroller,
17
+ getParentScroller,
18
+ getScrollTop,
19
+ getOffsetHeight,
20
+ scrollToY,
21
+ isEmpty,
22
+ setMousewheelSlow,
23
+ orderBy,
24
+ getColumnById
25
+ } from './util'
26
+
27
+ // 表格body class名称
28
+ const TableBodyClassNames = [
29
+ '.el-table__body-wrapper', // 主表格容器
30
+ '.el-table__fixed-right .el-table__fixed-body-wrapper', // 右固定表格容器
31
+ '.el-table__fixed .el-table__fixed-body-wrapper' // 左固定表格容器
32
+ ]
33
+
34
+ export default {
35
+ name: 'el-table-virtual-scroll',
36
+ props: {
37
+ // 总数据
38
+ data: {
39
+ type: Array,
40
+ required: true
41
+ },
42
+ // 每一行的预估高度
43
+ itemSize: {
44
+ type: Number,
45
+ default: 60
46
+ },
47
+ // 指定滚动容器
48
+ scrollBox: {
49
+ type: String
50
+ },
51
+ // 顶部和底部缓冲区域,值越大显示表格的行数越多
52
+ buffer: {
53
+ type: Number,
54
+ default: 200
55
+ },
56
+ // key值,data数据中的唯一id
57
+ keyProp: {
58
+ type: String,
59
+ default: 'id'
60
+ },
61
+ // 滚动事件的节流时间
62
+ throttleTime: {
63
+ type: Number,
64
+ default: 16
65
+ },
66
+ // 是否获取表格行动态高度
67
+ dynamic: {
68
+ type: Boolean,
69
+ default: true
70
+ },
71
+ // 是否开启虚拟滚动
72
+ virtualized: {
73
+ type: Boolean,
74
+ default: true
75
+ },
76
+ // 表格行合并时,合并在一起的行返回相同的key值
77
+ rowSpanKey: {
78
+ type: Function
79
+ },
80
+ warn: {
81
+ type: Boolean,
82
+ default: true
83
+ },
84
+ // 禁用虚拟滚动
85
+ disabled: {
86
+ type: Boolean,
87
+ default: false
88
+ },
89
+ // 支持自定义选中数据的排序规则,传入false则可保留列表的排序规则,默认是按照选中顺序排序
90
+ selectionSort: {
91
+ type: [Function, Boolean],
92
+ default: true
93
+ },
94
+ // 获取el-table组件,默认 virtual-scroll 组件的第一个子组件
95
+ getElTable: {
96
+ type: Function,
97
+ default: function () {
98
+ return this.$children[0]
99
+ }
100
+ },
101
+ keepScroll: {
102
+ type: Boolean,
103
+ default: true
104
+ }
105
+ },
106
+ provide () {
107
+ return {
108
+ virtualScroll: this
109
+ }
110
+ },
111
+ data () {
112
+ return {
113
+ sizes: {}, // 尺寸映射(依赖响应式)
114
+ start: 0, // 渲染列表开始索引
115
+ end: undefined, // 渲染列表结束索引
116
+ curRow: null, // 表格单选:选中的行
117
+ oldSelection: [], // 表格多选:选中的行
118
+ isExpanding: false, // 列是否正在展开
119
+ columnVms: [], // virtual-column 组件实例
120
+ isHideAppend: false, // 是否隐藏append
121
+ scrollPosition: '', // x轴滚动位置(左、中、右)
122
+ hasFixedRight: false, // 是否有固定右边的列
123
+ listData: [], // 未筛选为data源数据,筛选后则为筛选后的数据
124
+ isTree: false // 是否自定义树形表格
125
+ }
126
+ },
127
+ computed: {
128
+ // 计算出每个item(的key值)到滚动容器顶部的距离
129
+ offsetMap ({ keyProp, itemSize, sizes, listData }) {
130
+ if (!this.dynamic) return {}
131
+
132
+ const res = {}
133
+ let total = 0
134
+ for (let i = 0; i < listData.length; i++) {
135
+ const key = listData[i][keyProp]
136
+ if (typeof key === 'undefined') {
137
+ this.warn && console.warn(`data[${i}][${keyProp}] 为 undefined,请确保 keyProp 对应的值不为undefined`)
138
+ }
139
+ res[key] = total
140
+
141
+ const curSize = sizes[key]
142
+ const size = typeof curSize === 'number' ? curSize : itemSize
143
+ total += size
144
+ }
145
+ return res
146
+ },
147
+ // 树节点的 children 映射,通过响应式关联起来,那么children中添加、删除节点会触发treeMap computed,从而监听treeMap更新视图【注:children 添加删除不会触发data watch,data只是浅监听】
148
+ treeMap ({ data, keyProp, treeProps, isTree }) {
149
+ if (!isTree || !treeProps) return
150
+ const res = {}
151
+ const { children } = treeProps
152
+ const traverse = (nodes) => {
153
+ nodes.forEach(node => {
154
+ const key = node[keyProp]
155
+ if (typeof key !== 'undefined' && node[children]) {
156
+ res[key] = node[children]
157
+ traverse(node[children])
158
+ }
159
+ })
160
+ }
161
+
162
+ // 开始遍历树结构
163
+ traverse(data)
164
+ return res
165
+ }
166
+ },
167
+ methods: {
168
+ // 初始化数据
169
+ initData () {
170
+ this.destory() // 销毁,防止多次调用
171
+ // 可视范围内显示数据
172
+ this.renderData = []
173
+ // 页面可视范围顶端、底部
174
+ this.top = undefined
175
+ this.bottom = undefined
176
+ // 截取页面可视范围内显示数据的开始和结尾索引
177
+ this.start = 0
178
+ this.end = undefined
179
+ // 是否是表格内部滚动
180
+ this.isInnerScroll = false
181
+ // 高亮的行
182
+ this.highlightRow = null
183
+ // 滚动位置
184
+ this.scrollPos = [0, 0]
185
+ // 触发scroll
186
+ this.triggleScroll = false
187
+ // 多选:记录多选选项的顺序
188
+ this.checkOrder = 0
189
+
190
+ // 验证ElTable组件
191
+ this.elTable = this.getElTable()
192
+ if (!this.elTable || this.elTable.$options.name !== 'ElTable') {
193
+ throw new Error('未找到 <el-table> 组件. 请确保 <el-table> 组件在虚拟组件内,且 getElTable 方法能获取到正确的 <el-table> 组件!')
194
+ }
195
+ if (!this.elTable.rowKey) {
196
+ this.warn && console.warn('[el-table-virtual-scroll]: 建议设置 <el-table> 组件的 rowKey 属性')
197
+ }
198
+
199
+ this.scroller = this.getScroller()
200
+ this.observeElTable()
201
+
202
+ // 监听事件
203
+ this.onScroll = !this.throttleTime ? this.handleScroll : throttle(this.handleScroll, this.throttleTime)
204
+ this.scroller.addEventListener('scroll', this.onScroll)
205
+ window.addEventListener('resize', this.onScroll)
206
+
207
+ // 兼容
208
+ this.hackTableExpand() // 兼容表格展开行
209
+ this.hackTableHeaderDrag() // 兼容表格头拖拽
210
+ this.hackTableSort() // 兼容表格排序
211
+ this.hackTableFilter() // 兼容表格筛选
212
+ this.hackRowHighlight() // 兼容单选
213
+ this.hackSelection() // 兼容多选
214
+ this.hackCustomTree() // 兼容树形表格
215
+ this.bindTableDestory() // 绑定表格销毁事件
216
+
217
+ // 设置listData,首次updateTreeData会在node上添加$v_tree属性,触发data watch,从而触发 onSortChange,所以defaultSort就无需再次触发
218
+ this.treeProps = this.elTable.treeProps || { children: 'children', hasChildren: 'hasChildren' }
219
+ // 此处兼容 default-sort 属性
220
+ if (this.elTable.defaultSort) {
221
+ this.$nextTick(() => { // 此处使用nextTick是因为 el-tale的sortingColumn排序数据还没设置好,得等一会
222
+ this.onSortChange() // onSortChange 会触发updateTreeData
223
+ })
224
+ } else {
225
+ this.updateTreeData()
226
+ }
227
+
228
+ // 初次执行 (固定高度的表格布局好后,会触发 bodyHeight 更改(已手动监听,位于 unWatch2代码处),从而触发 onScroll,所以无需手动执行onScroll)
229
+ setTimeout(() => {
230
+ !this.triggleScroll && this.onScroll()
231
+ }, 100)
232
+ },
233
+
234
+ // 滚轮滚动速度减缓,减少快速滚动白屏
235
+ // slowNum - 减速的值,值越大,滚动越慢
236
+ slowOnMousewheel (slowNum = 1, scroller = this.scroller) {
237
+ this.removeMousewheelEvent && this.removeMousewheelEvent()
238
+ this.removeMousewheelEvent = null
239
+
240
+ if (!slowNum) return
241
+ this.removeMousewheelEvent = setMousewheelSlow(scroller, slowNum)
242
+ },
243
+
244
+ // 获取滚动元素
245
+ getScroller () {
246
+ let el
247
+ if (this.scrollBox) {
248
+ if (this.scrollBox === 'window' || this.scrollBox === window) return window
249
+
250
+ el = document.querySelector(this.scrollBox)
251
+ if (!el) throw new Error(` scrollBox prop: '${this.scrollBox}' is not a valid selector`)
252
+ if (!isScroller(el)) console.warn(`Warning! scrollBox prop: '${this.scrollBox}' is not a scroll element`)
253
+ return el
254
+ }
255
+ // 如果表格是固定高度,则获取表格内的滚动节点,否则获取父层滚动节点
256
+ if (this.elTable && (this.elTable.height || this.elTable.maxHeight || this.elTable.height === 0 || this.elTable.maxHeight === 0)) {
257
+ this.isInnerScroll = true
258
+ return this.$el.querySelector('.el-table__body-wrapper')
259
+ } else {
260
+ return getParentScroller(this.$el)
261
+ }
262
+ },
263
+
264
+ // 设置表格到滚动容器的距离
265
+ getToTop () {
266
+ if (this.isInnerScroll) {
267
+ return 0
268
+ } else {
269
+ return this.$el.getBoundingClientRect().top - (this.scroller === window ? 0 : this.scroller.getBoundingClientRect().top) + getScrollTop(this.scroller)
270
+ }
271
+ },
272
+
273
+ // 处理滚动事件
274
+ handleScroll (shouldUpdate = true) {
275
+ if (this.disabled) return
276
+ if (!this.scroller) return
277
+ this.triggleScroll = true
278
+
279
+ // 【修复】如果使用v-show 进行切换表格会特别卡顿 #30;
280
+ // 【原因】v-show为false时,表格内滚动容器的高度为auto,没有滚动条限制,虚拟滚动计算渲染全部内容
281
+ if (this.isInnerScroll && !this.scroller.style.height && !this.scroller.style.maxHeight) return
282
+
283
+ // 如果组件失活,则不再执行handleScroll;否则外部容器滚动情况下记录的scrollTop会是0
284
+ if (this.isDeactivated) return
285
+ // 记录scrollPos
286
+ // 需要判断表格没有隐藏(修复表格隐藏状态下更新绑定数组长度,显示后滚动条位置异常 #67)
287
+ if (this.isInnerScroll && this.elTable.layout.bodyHeight) {
288
+ this.scrollPos[0] = this.scroller.scrollTop
289
+ this.scrollPos[1] = this.scroller.scrollLeft
290
+ }
291
+ if (!this.virtualized) return
292
+
293
+ this.removeHoverRows()
294
+ // 更新当前尺寸(高度)
295
+ this.updateSizes()
296
+ // 计算renderData
297
+ this.calcRenderData()
298
+ // 计算位置
299
+ this.calcPosition()
300
+ shouldUpdate && this.updatePosition()
301
+ // 触发事件
302
+ this.$emit('change', this.renderData, this.start, this.end)
303
+ // 同步表格行高亮
304
+ this.syncRowsHighlight()
305
+ },
306
+
307
+ // 移除多个hover-row
308
+ removeHoverRows () {
309
+ const hoverRows = this.$el.querySelectorAll('.el-table__row.hover-row')
310
+ if (hoverRows.length > 1) {
311
+ Array.from(hoverRows).forEach((row) => {
312
+ row.classList.remove('hover-row')
313
+ })
314
+ }
315
+ },
316
+
317
+ // 更新尺寸(高度)
318
+ updateSizes () {
319
+ if (!this.dynamic) return
320
+ let rows = this.$el.querySelectorAll('.el-table__body > tbody > .el-table__row')
321
+
322
+ // 处理树形表格(修复树结构懒加载 如果有hasChildren=false的行 行可视区域高度异常 #45)
323
+ const isTree = this.elTable.lazy
324
+ const isVTree = this.isTree // 自定义树(非el-table的树)
325
+ const noFirstLevelReg = /el-table__row--level-[1-9]\d*/ // 匹配树形表格非一级行
326
+ if (!isVTree && isTree) {
327
+ // 筛选出树形表格的一级行,一级行className含有el-table__row--level-0或者不存在层级className
328
+ rows = Array.from(this.$el.querySelectorAll('.el-table__body > tbody > .el-table__row')).filter(row => {
329
+ return !noFirstLevelReg.test(row.className)
330
+ })
331
+ }
332
+
333
+ Array.from(rows).forEach((row, index) => {
334
+ const item = this.renderData[index]
335
+ if (!item) return
336
+
337
+ // 计算表格行的高度
338
+ let offsetHeight = row.offsetHeight
339
+ // 表格行如果有扩展行,需要加上扩展内容的高度
340
+ if (!isTree && !isVTree && row.classList.contains('expanded')) {
341
+ offsetHeight += row.nextSibling.offsetHeight
342
+ }
343
+ // 表格行如果有子孙节点,需要加上子孙节点的高度
344
+ if (isTree) {
345
+ let next = row.nextSibling
346
+ while (next && next.tagName === 'TR' && noFirstLevelReg.test(next.className)) {
347
+ offsetHeight += next.offsetHeight
348
+ next = next.nextSibling
349
+ }
350
+ }
351
+
352
+ const key = item[this.keyProp]
353
+ if (offsetHeight && this.sizes[key] !== offsetHeight) {
354
+ this.$set(this.sizes, key, offsetHeight)
355
+ }
356
+ })
357
+ },
358
+
359
+ // 获取某条数据offsetTop
360
+ getItemOffsetTop (index) {
361
+ if (!this.dynamic) {
362
+ return this.itemSize * index
363
+ }
364
+
365
+ const item = this.listData[index]
366
+ if (item) {
367
+ return this.offsetMap[item[this.keyProp]] || 0
368
+ }
369
+ return 0
370
+ },
371
+
372
+ // 获取某条数据的尺寸
373
+ getItemSize (index) {
374
+ if (index <= -1) return 0
375
+ const item = this.listData[index]
376
+ if (item) {
377
+ const key = item[this.keyProp]
378
+ return this.sizes[key] || this.itemSize
379
+ }
380
+ return this.itemSize
381
+ },
382
+
383
+ // 计算只在视图上渲染的数据
384
+ calcRenderData () {
385
+ const { scroller, listData, buffer } = this
386
+ // 计算可视范围顶部、底部
387
+ const toTop = this.getToTop() // 表格到滚动容器的距离
388
+ const top = getScrollTop(scroller) - buffer - toTop
389
+ const bottom = getScrollTop(scroller) + getOffsetHeight(scroller) + buffer - toTop
390
+
391
+ let start
392
+ let end
393
+ if (!this.dynamic) {
394
+ start = top <= 0 ? 0 : Math.floor(top / this.itemSize)
395
+ end = bottom <= 0 ? 0 : Math.ceil(bottom / this.itemSize)
396
+ } else {
397
+ // 二分法计算可视范围内的开始的第一个内容
398
+ let l = 0
399
+ let r = listData.length - 1
400
+ let mid = 0
401
+ while (l <= r) {
402
+ mid = Math.floor((l + r) / 2)
403
+ const midVal = this.getItemOffsetTop(mid)
404
+ if (midVal < top) {
405
+ const midNextVal = this.getItemOffsetTop(mid + 1)
406
+ if (midNextVal > top) break
407
+ l = mid + 1
408
+ } else {
409
+ r = mid - 1
410
+ }
411
+ }
412
+ start = mid
413
+
414
+ // 二分法计算可视范围内的结束的最后一个内容
415
+ l = start
416
+ r = listData.length - 1
417
+ mid = 0
418
+ while (l <= r) {
419
+ mid = Math.floor((l + r) / 2)
420
+ const midVal = this.getItemOffsetTop(mid)
421
+ if (midVal >= bottom) {
422
+ const midNextVal = this.getItemOffsetTop(mid - 1)
423
+ if (midNextVal < bottom) break
424
+ r = mid - 1
425
+ } else {
426
+ l = mid + 1
427
+ }
428
+ }
429
+ end = mid
430
+ }
431
+
432
+ if (this.isRowSpan()) {
433
+ // 计算包含合并行的开始结束区间(⚠️注意:合并行不支持使用斑马纹,因为不能100%确定合并行的开始行是偶数,可能会向上找一直到第一行,导致渲染非常多行,浪费性能)
434
+ [start, end] = this.calcRenderSpanData(start, end)
435
+ } else {
436
+ // 开始索引始终保持偶数,如果为奇数,则加1使其保持偶数【确保表格行的偶数数一致,不会导致斑马纹乱序显示】
437
+ if (start % 2) start = start - 1
438
+ }
439
+
440
+ this.top = top
441
+ this.bottom = bottom
442
+ this.start = start
443
+ this.end = end
444
+ this.renderData = listData.slice(start, end + 1)
445
+ if (this.start === 0 && this.end > 30 && this.end === this.listData.length - 1) {
446
+ this.warn && console.warn('[el-table-virtual-scroll] 表格数据全部渲染,渲染数量为:' + this.listData.length)
447
+ }
448
+ },
449
+
450
+ // 是否是合并行
451
+ isRowSpan () {
452
+ return typeof this.rowSpanKey === 'function'
453
+ },
454
+
455
+ // 如果存在合并行的情况,渲染的数据范围扩大到包含合并行
456
+ calcRenderSpanData (start, end) {
457
+ // 从开始节点向上查找是否有合并行
458
+ let prevKey
459
+ while (start > 0) {
460
+ const curRow = this.listData[start]
461
+ const curkey = this.rowSpanKey(curRow, start)
462
+ // 如果不存在key,说明当前行不属于合并行
463
+ if (isEmpty(curkey)) break
464
+
465
+ // 如果当前行与后面一行的key不相同,说明则当前行不属于合并行,从后一行开始截断
466
+ if (!isEmpty(prevKey) && prevKey !== curkey) {
467
+ start++
468
+ break
469
+ }
470
+
471
+ prevKey = curkey
472
+ start--
473
+ }
474
+
475
+ // 从末端节点向下查找是否有合并行
476
+ const len = this.listData.length
477
+ prevKey = undefined
478
+ while (end < len) {
479
+ const curRow = this.listData[end]
480
+ const curkey = this.rowSpanKey(curRow, end)
481
+ // 如果不存在key,说明当前行不属于合并行
482
+ if (!curkey) break
483
+
484
+ // 如果当前行与前面一行的key不相同,说明则当前行不属于合并行,从前一行开始截断
485
+ if (prevKey && prevKey !== curkey) {
486
+ end--
487
+ break
488
+ }
489
+
490
+ prevKey = curkey
491
+ end++
492
+ }
493
+
494
+ return [start, end]
495
+ },
496
+
497
+ // 计算位置
498
+ calcPosition () {
499
+ const last = this.listData.length - 1
500
+ // 计算内容总高度
501
+ const wrapHeight = this.getItemOffsetTop(last) + this.getItemSize(last)
502
+ // 计算当前滚动位置需要撑起的高度
503
+ const offsetTop = this.getItemOffsetTop(this.start)
504
+
505
+ let tableWrapEl
506
+ // 设置dom位置
507
+ TableBodyClassNames.forEach((className, index) => {
508
+ const el = this.$el.querySelector(className)
509
+ if (!el) return
510
+
511
+ // 创建wrapEl、innerEl
512
+ if (!el.wrapEl) {
513
+ const wrapEl = document.createElement('div')
514
+ const innerEl = document.createElement('div')
515
+ wrapEl.appendChild(innerEl)
516
+ innerEl.appendChild(el.children[0])
517
+ el.insertBefore(wrapEl, el.firstChild)
518
+ el.wrapEl = wrapEl
519
+ el.innerEl = innerEl
520
+
521
+ // 修复 fixed 动态切换时,固定列不显示(scrollTop位置和非固定列滚动位置不一致导致的,需要同步scrollTop的值)
522
+ if (index > 0 && tableWrapEl) {
523
+ this.$nextTick(() => {
524
+ el.scrollTop = tableWrapEl.scrollTop
525
+ })
526
+ }
527
+ }
528
+ index === 0 && (tableWrapEl = el) // 记录非固定列的dom
529
+
530
+ if (el.wrapEl) {
531
+ // 设置高度
532
+ el.wrapEl.style.height = wrapHeight + 'px'
533
+ // 设置transform撑起高度
534
+ el.innerEl.style.transform = `translateY(${offsetTop}px)`
535
+ // 设置paddingTop撑起高度
536
+ // el.innerEl.style.paddingTop = `${offsetTop}px`
537
+ }
538
+ })
539
+ },
540
+
541
+ // 监听el-table
542
+ observeElTable () {
543
+ // 监听滚动位置
544
+ const unWatch1 = this.$watch(
545
+ () => [this.elTable.scrollPosition, this.elTable.layout.scrollX],
546
+ ([pos, scrollX], [oldPos, oldScrollX] = []) => {
547
+ // 修复自定义固定列 所有列宽总宽度小于表格宽度时 固定列样式有问题 #65
548
+ this.scrollPosition = this.elTable.layout.scrollX ? pos : 'none'
549
+
550
+ // 修复element-ui原有bug:当窗口缩放时,x轴滚动条从无到到有,且x轴已滚动到最右侧,右侧固定列
551
+ if (scrollX && !oldScrollX) {
552
+ this.elTable.syncPostion && this.elTable.syncPostion()
553
+ }
554
+ }, { immediate: true })
555
+
556
+ // 监听表格滚动高度变化(切换v-show时更新)
557
+ const unWatch2 = this.$watch(() => this.elTable.layout.bodyHeight, (val) => {
558
+ val > 0 && this.restoreScroll()
559
+ val > 0 && this.onScroll()
560
+ })
561
+ this.unWatchs = [unWatch1, unWatch2]
562
+ },
563
+
564
+ // 执行update方法更新虚拟滚动,且每次nextTick只能执行一次【在数据大于100条开启虚拟滚动时,由于监听了data、virtualized会连续触发两次update方法:第一次update时,(updateSize)计算尺寸里的渲染数据(renderData)与表格行的dom是一一对应,之后会改变渲染数据(renderData)的值;而第二次执行update时,renderData改变了,而表格行dom未改变,导致renderData与dom不一一对应,从而位置计算错误,最终渲染的数据对应不上。因此使用每次nextTick只能执行一次来避免bug发生】
565
+ doUpdate () {
566
+ if (this.hasDoUpdate) return // nextTick内已经执行过一次就不执行
567
+ if (!this.scroller) return // scroller不存在说明未初始化完成,不执行
568
+
569
+ // 启动虚拟滚动的瞬间,需要暂时隐藏el-table__append-wrapper里的内容,不然会导致滚动位置一直到append的内容处
570
+ this.isHideAppend = true
571
+ this.onScroll()
572
+ this.hasDoUpdate = true
573
+ this.$nextTick(() => {
574
+ this.hasDoUpdate = false
575
+ this.isHideAppend = false
576
+ })
577
+ },
578
+
579
+ // 空闲时更新位置(触发时间:滚动停止后等待10ms执行)
580
+ // 场景:固定表格fixed变化时、扩展行展开滑动时需要更新
581
+ updatePosition () {
582
+ this.timer && clearTimeout(this.timer)
583
+ this.timer = setTimeout(() => {
584
+ this.timer && clearTimeout(this.timer)
585
+ // 传入false,避免一直循环调用
586
+ this.handleScroll(false)
587
+ }, this.throttleTime + 10)
588
+ },
589
+
590
+ // 渲染全部数据
591
+ renderAllData () {
592
+ this.renderData = this.listData
593
+ this.$emit('change', this.listData, 0, this.listData.length - 1)
594
+ console.log('renderAllData', this.listData)
595
+
596
+ this.$nextTick(() => {
597
+ // 清除撑起的高度和位置
598
+ TableBodyClassNames.forEach(className => {
599
+ const el = this.$el.querySelector(className)
600
+ if (!el) return
601
+
602
+ if (el.wrapEl) {
603
+ // 设置高度
604
+ el.wrapEl.style.height = 'auto'
605
+ // 设置transform撑起高度
606
+ el.innerEl.style.transform = `translateY(${0}px)`
607
+ }
608
+ })
609
+ })
610
+ },
611
+
612
+ // 恢复滚动位置(仅支持表格内部滚动)
613
+ restoreScroll () {
614
+ if (!this.scroller || !this.isInnerScroll) return
615
+ this.scroller.scrollLeft = this.keepScroll ? this.scrollPos[1] : 0
616
+ this.scroller.scrollTop = this.keepScroll ? this.scrollPos[0] : 0
617
+ },
618
+
619
+ // 【外部调用】更新
620
+ update () {
621
+ if (this.isTree) {
622
+ this.onFilterChange && this.onFilterChange()
623
+ }
624
+ // console.log('update')
625
+ this.handleScroll()
626
+ },
627
+
628
+ // 【外部调用】滚动到第几行
629
+ // (不太精确:滚动到第n行时,如果周围的表格行计算出真实高度后会更新高度,导致内容坍塌或撑起)
630
+ // offsetY - 偏移量
631
+ scrollTo (index, offsetY = 0, stop = false) {
632
+ const item = this.listData[index]
633
+ if (item && this.scroller) {
634
+ this.updateSizes()
635
+ this.calcRenderData()
636
+
637
+ this.$nextTick(() => {
638
+ const offsetTop = this.getItemOffsetTop(index) - offsetY
639
+ scrollToY(this.scroller, offsetTop)
640
+
641
+ // 调用两次scrollTo,第一次滚动时,如果表格行初次渲染高度发生变化时,会导致滚动位置有偏差,此时需要第二次执行滚动,确保滚动位置无误
642
+ if (!stop) {
643
+ setTimeout(() => {
644
+ this.scrollTo(index, offsetY, true)
645
+ }, 50)
646
+ }
647
+ })
648
+ }
649
+ },
650
+
651
+ // 【外部调用】滚动到对应的行
652
+ scrollToRow (row, offsetY = 0) {
653
+ const index = this.listData.findIndex((item) => item === row || item[this.keyProp] === row[this.keyProp])
654
+ this.scrollTo(index, offsetY)
655
+ },
656
+
657
+ // 【外部调用】重置 (没用废弃)
658
+ reset () {
659
+ this.sizes = {}
660
+ this.scrollTo(0, 0, false)
661
+ },
662
+
663
+ // 销毁
664
+ destory () {
665
+ if (this.scroller) {
666
+ this.scroller.removeEventListener('scroll', this.onScroll)
667
+ window.removeEventListener('resize', this.onScroll)
668
+ }
669
+ if (this.unWatchs) {
670
+ this.unWatchs.forEach(unWatch => unWatch())
671
+ }
672
+ if (this.removeMousewheelEvent) {
673
+ this.removeMousewheelEvent()
674
+ }
675
+ this.oldSelection = []
676
+ this.elTable = null
677
+ this.scroller = null
678
+ this.unWatchs = []
679
+ },
680
+
681
+ // 【VirtualColumn调用】获取列表全部数据】
682
+ // origin - 源数据,非筛选后的数据
683
+ getData (origin = true) {
684
+ return this.list || (origin ? this.data : this.listData)
685
+ },
686
+
687
+ // 【VirtualColumn调用】添加virtual-column实例
688
+ addColumn (vm) {
689
+ this.columnVms.push(vm)
690
+ },
691
+
692
+ // 【VirtualColumn调用】移除virtual-column实例
693
+ removeColumn (vm) {
694
+ this.columnVms = this.columnVms.filter(item => item !== vm)
695
+ },
696
+
697
+ // 【多选】选中所有列
698
+ checkAll (val, rows = this.listData, byUser = false) {
699
+ const removedRows = []
700
+ rows.forEach(row => {
701
+ if (row.$v_checked) {
702
+ removedRows.push(row)
703
+ }
704
+ if (row.$v_checked !== val) {
705
+ this.$set(row, '$v_checked', val)
706
+ this.$set(row, '$v_checkedOrder', val ? this.checkOrder++ : undefined)
707
+ }
708
+ })
709
+ const selection = this.emitSelectionChange(removedRows)
710
+
711
+ if (byUser) { // 当用户手动勾选全选 Checkbox 时触发的事件
712
+ this.$emit('select-all', selection, val)
713
+ this.elTable.$emit('select-all', selection, val)
714
+ }
715
+ if (val === false) {
716
+ this.checkOrder = 0 // 取消全选,则重置checkOrder
717
+ }
718
+ },
719
+
720
+ // 【多选】选中某一列
721
+ checkRow (row, val, emit = true, byUser = false) {
722
+ if (row.$v_checked === val) return
723
+
724
+ this.$set(row, '$v_checked', val)
725
+ this.$set(row, '$v_checkedOrder', val ? this.checkOrder++ : undefined)
726
+ if (emit) {
727
+ const selection = this.emitSelectionChange(val ? [] : [row])
728
+ if (byUser) { // 当用户手动勾选数据行的 Checkbox 时触发的事件
729
+ this.$emit('select', selection, row, val)
730
+ this.elTable.$emit('select', selection, row, val)
731
+ }
732
+ }
733
+ },
734
+
735
+ // 【多选】兼容表格clearSelection方法
736
+ clearSelection () {
737
+ // 清除旧的选中项
738
+ this.oldSelection.forEach(row => {
739
+ this.$set(row, '$v_checked', false)
740
+ })
741
+ this.oldSelection = []
742
+
743
+ // 清除所有选中项
744
+ this.checkAll(false)
745
+ this.columnVms.forEach(vm => vm.syncCheckStatus())
746
+ },
747
+
748
+ // 【多选】兼容表格toggleRowSelection方法
749
+ toggleRowSelection (row, selected) {
750
+ if (!Array.isArray(row)) {
751
+ row = [row]
752
+ }
753
+ this.toggleRowsSelection(row, selected)
754
+ },
755
+
756
+ // 【扩展多选】表格切换多个row选中状态
757
+ toggleRowsSelection (rows, selected) {
758
+ // reserve-selection 模式用到的变量
759
+ const oldSelectedMap = {} // 保留值map(旧的选中值)
760
+ const curSelectedMap = {} // 当前选中值map
761
+ let toDeleteMap = null // 需删除值map
762
+ const isReserve = this.isReserveSelection()
763
+ if (isReserve) {
764
+ this.oldSelection.forEach(row => {
765
+ oldSelectedMap[row[this.keyProp]] = true
766
+ })
767
+ this.data.forEach(row => {
768
+ curSelectedMap[row[this.keyProp]] = true
769
+ })
770
+ }
771
+
772
+ const removedRows = []
773
+ rows.forEach(row => {
774
+ const val = typeof selected === 'boolean' ? selected : !row.$v_checked
775
+ !val && removedRows.push(row)
776
+ this.$set(row, '$v_checked', val)
777
+ this.$set(row, '$v_checkedOrder', val ? this.checkOrder++ : undefined)
778
+ if (!isReserve) return
779
+
780
+ /* 处理reserve-selection 模式 */
781
+ // 如果row在保留值oldSelection里,且取消选中,则需要将它移除
782
+ const key = row[this.keyProp]
783
+ if (key in oldSelectedMap && !val) {
784
+ if (!toDeleteMap) toDeleteMap = {}
785
+ toDeleteMap[key] = true
786
+ }
787
+ // 如果row不在当前列表里,且为选中,则需要添加在保留值oldSelection里
788
+ if (!(key in curSelectedMap) && val) {
789
+ this.oldSelection.push(row)
790
+ }
791
+ })
792
+ if (toDeleteMap) {
793
+ this.oldSelection = this.oldSelection.filter(
794
+ row => !(row[this.keyProp] in toDeleteMap)
795
+ )
796
+ }
797
+
798
+ this.emitSelectionChange(removedRows)
799
+ this.columnVms.forEach(vm => vm.syncCheckStatus())
800
+ },
801
+
802
+ // 【多选】兼容表格selection-change事件
803
+ emitSelectionChange (removedRows) {
804
+ const isReserve = this.isReserveSelection() // 是否保留旧的值
805
+ const selection = isReserve ? [...this.oldSelection] : []
806
+ this.data.forEach(row => {
807
+ if (row.$v_checked) {
808
+ selection.push(row)
809
+ }
810
+ })
811
+ this.sortSelection(selection)
812
+ this.$emit('selection-change', selection, removedRows)
813
+ this.elTable.$emit('selection-change', selection, removedRows)
814
+ // 对于 reserve-selection 模式,oldSelection始终保留旧的选中值,不保留当前选中值
815
+ // 对于 非 reserve-selection 模式,oldSelection始终保留当前选中值
816
+ if (!isReserve) {
817
+ this.oldSelection = [...selection]
818
+ }
819
+ return selection
820
+ },
821
+
822
+ // 【多选】兼容表格 reserve-selection,存储上次选中的值
823
+ updateSelectionByRowKey (data, oldData = []) {
824
+ if (!this.elTable) return
825
+
826
+ // 将旧的选中值存储到oldSelection中
827
+ oldData.forEach(row => {
828
+ if (row.$v_checked) {
829
+ this.oldSelection.push(row)
830
+ }
831
+ })
832
+ const selectedMap = {}
833
+ this.oldSelection.forEach(row => {
834
+ selectedMap[row[this.keyProp]] = true
835
+ })
836
+
837
+ const usedMap = {} // 当前选中值的map
838
+ data.forEach(row => {
839
+ const key = row[this.keyProp]
840
+ if (key in selectedMap) {
841
+ this.$set(row, '$v_checked', true)
842
+ usedMap[key] = true
843
+ }
844
+ })
845
+
846
+ // 从oldSelection中移除当前选中的值
847
+ this.oldSelection = this.oldSelection.filter(row => !(row[this.keyProp] in usedMap))
848
+ },
849
+
850
+ // 【多选】多选列是否设置了 reserve-selection
851
+ isReserveSelection () {
852
+ return this.columnVms.some(vm => vm.reserveSelection && vm.isSelection())
853
+ },
854
+
855
+ // 获取选中值
856
+ getSelection () {
857
+ if (this.isReserveSelection()) {
858
+ const curSelection = this.data.filter(row => row.$v_checked)
859
+ return [...this.oldSelection, ...curSelection]
860
+ } else {
861
+ return this.oldSelection
862
+ }
863
+ },
864
+
865
+ // 【多选】更新多选的值
866
+ updateSelectionData (data, oldData) {
867
+ this.syncSelectionStatus()
868
+
869
+ if (data !== oldData) {
870
+ if (this.oldSelection.length > 0) {
871
+ // 修复多选select-change事件在表格数据更新后未触发,导致旧数据未清除 #100
872
+ this.$emit('selection-change', [], [...this.oldSelection])
873
+ this.elTable.$emit('selection-change', [], [...this.oldSelection])
874
+ this.oldSelection = []
875
+ }
876
+ return
877
+ }
878
+
879
+ // 新的选中项
880
+ const selection = this.data.filter(row => row.$v_checked)
881
+ this.sortSelection(selection)
882
+ // 新的选中项key map
883
+ const selectionKeyMap = selection.reduce((map, dataItem) => {
884
+ map[dataItem[this.keyProp]] = true
885
+ return map
886
+ }, {})
887
+ // 移除的项
888
+ const removedRows = this.oldSelection.reduce((rows, row) => {
889
+ if (!(row[this.keyProp] in selectionKeyMap)) rows.push(row)
890
+ return rows
891
+ }, [])
892
+ // 手动删除选中项、新旧项不一致(正常不会发生),触发selection-change事件
893
+ if (removedRows.length || selection.length !== this.oldSelection.length) {
894
+ this.$emit('selection-change', selection, removedRows)
895
+ this.elTable.$emit('selection-change', selection, removedRows)
896
+ this.oldSelection = [...selection]
897
+ }
898
+ },
899
+
900
+ // 【多选】多选排序
901
+ sortSelection (selection) {
902
+ if (!this.selectionSort) return
903
+ if (typeof this.selectionSort === 'function') {
904
+ selection.sort((a, b) => this.selectionSort(a, b))
905
+ } else {
906
+ selection.sort((a, b) => a.$v_checkedOrder - b.$v_checkedOrder)
907
+ }
908
+ },
909
+
910
+ // 【多选】同步多选状态
911
+ syncSelectionStatus () {
912
+ const selectionVm = this.columnVms.find(vm => vm.isSelection())
913
+ if (selectionVm) {
914
+ selectionVm.syncCheckStatus()
915
+ }
916
+ },
917
+
918
+ // 【radio单选】设置选中行
919
+ setCurrentRow (row) {
920
+ this.curRow = row
921
+ this.$emit('current-change', row)
922
+ this.elTable.$emit('current-change', row)
923
+ },
924
+
925
+ // 【单选高亮】兼容行高亮
926
+ hackRowHighlight () {
927
+ // 兼容el-table的setCurrentRow:重写setCurrentRow方法
928
+ if (this.elTable.setCurrentRow.virtual) {
929
+ const originSetCurrentRow = this.elTable.setCurrentRow.bind(this.elTable) // 原方法
930
+ const setCurrentRow = (row) => { // 重写setCurrentRow
931
+ this.elTable.store.states.currentRow = this.highlightRow // 同步表格行高亮的值
932
+ if (this.highlightRow !== row) this.highlightRow = row // 同步highlightRow的值
933
+ originSetCurrentRow(row) // 执行原方法
934
+ }
935
+ this.elTable.setCurrentRow = setCurrentRow
936
+ setCurrentRow.virtual = true
937
+ }
938
+ // 兼容el-table的currentRowKey属性
939
+ const unWatch = this.$watch(() => this.elTable.currentRowKey, (val) => {
940
+ if (this.elTable.rowKey) {
941
+ const targetRow = this.listData.find(row => val === row[this.elTable.rowKey])
942
+ this.highlightRow = targetRow
943
+ }
944
+ }, { immediate: true })
945
+ this.unWatchs.push(unWatch)
946
+
947
+ // 监听高亮的事件
948
+ const onCurrentChange = (row) => {
949
+ this.highlightRow = row
950
+ }
951
+ this.elTable.$on('current-change', onCurrentChange)
952
+ this.unWatchs.push(() => {
953
+ this.elTable.$off('current-change', onCurrentChange)
954
+ })
955
+ },
956
+
957
+ // 【单选高亮】同步表格行高亮的值
958
+ syncRowsHighlight () {
959
+ if (!this.elTable.highlightCurrentRow) return
960
+ // 必须使用nextTick,不然值同步不上
961
+ this.$nextTick(() => {
962
+ this.elTable.store.states.currentRow = this.highlightRow
963
+ })
964
+ },
965
+
966
+ // 使用自定义的树形表格;需禁用原来的树(将children字段改为空)
967
+ useCustomSelection () {
968
+ this.isSelection = true
969
+ },
970
+
971
+ // 【多选高亮】兼容多选
972
+ hackSelection () {
973
+ // 兼容选中行高亮
974
+ const onFixedColumnsChange = () => {
975
+ if (!this.elTable) return
976
+ const tableBodyVm = this.elTable.$children.filter(vm => {
977
+ return vm.$options.name === 'ElTableBody'
978
+ })
979
+ tableBodyVm.forEach(vm => {
980
+ if (vm.getRowClass.virtual) return
981
+ // 重写 table-body组件的 getRowClass 方法
982
+ // 支持多选选中时高亮
983
+ const originGetRowClass = vm.getRowClass.bind(this.elTable)
984
+ vm.getRowClass = (row, rowIndex) => {
985
+ const classes = originGetRowClass(row, rowIndex)
986
+ if (this.elTable.highlightSelectionRow && row.$v_checked) {
987
+ classes.push('selection-row')
988
+ }
989
+ return classes
990
+ }
991
+ vm.getRowClass.virtual = true
992
+ })
993
+ }
994
+ const unWatch = this.$watch(
995
+ () => [this.elTable.fixedColumns, this.elTable.rightFixedColumns],
996
+ onFixedColumnsChange,
997
+ { immediate: true }
998
+ )
999
+ this.unWatchs.push(unWatch)
1000
+
1001
+ // 兼容多选 toggleRowSelection 方法
1002
+ this.elTable.toggleRowSelection = (row, selected) => {
1003
+ this.toggleRowSelection(row, selected)
1004
+ }
1005
+
1006
+ this.elTable.clearSelection = () => {
1007
+ this.clearSelection()
1008
+ }
1009
+
1010
+ // 【多选】兼容表格 toggleAllSelection 方法
1011
+ this.elTable.toggleAllSelection = () => {
1012
+ const selectionVm = this.columnVms.find(vm => vm.isSelection())
1013
+ if (selectionVm) {
1014
+ selectionVm.onCheckAllRows(!selectionVm.isCheckedAll)
1015
+ }
1016
+ }
1017
+ },
1018
+
1019
+ // 监听表格header-dragend事件
1020
+ hackTableHeaderDrag () {
1021
+ const onHeaderDragend = () => {
1022
+ // 设置状态,用于自定义固定列
1023
+ this.hasHeadDrag = true
1024
+ setTimeout(() => {
1025
+ // #50 修复el-table原bug: 刷新布局,列放大缩小让高度变大,导致布局错乱
1026
+ this.elTable.doLayout()
1027
+ })
1028
+ // 修复某一行内容很多时,将该行宽度拖拽成很宽,内容坍塌导致空白行(需要立即更新,因为要获取新行变化的高度)
1029
+ this.update()
1030
+ }
1031
+ this.elTable.$on('header-dragend', onHeaderDragend)
1032
+ this.unWatchs.push(() => {
1033
+ this.elTable.$off('header-dragend', onHeaderDragend)
1034
+ })
1035
+ },
1036
+
1037
+ // 【展开行】使用扩展行
1038
+ useExpandTable () {
1039
+ this.isExpandType = true
1040
+ },
1041
+
1042
+ // 【展开行】监听表格expand-change事件
1043
+ hackTableExpand () {
1044
+ if (!this.isExpandType) return
1045
+ const { store } = this.elTable
1046
+
1047
+ // 判断表格行是否渲染扩展行
1048
+ store.isRowExpanded = (row) => {
1049
+ return row.$v_expanded
1050
+ }
1051
+
1052
+ // expandRowKeys 变化时候,设置展开的行
1053
+ store.setExpandRowKeys = () => {
1054
+ const { expandRowKeys } = this.elTable
1055
+ this.listData.forEach(row => {
1056
+ this.$set(row, '$v_expanded', expandRowKeys.includes(row[this.elTable.rowKey]))
1057
+ })
1058
+ }
1059
+
1060
+ // 表格data变化时,更新展开的行
1061
+ store.updateExpandRows = () => {
1062
+ const { defaultExpandAll } = this.elTable
1063
+ if (defaultExpandAll) {
1064
+ // 当设置了默认展开所有行时,直接设置所有行展开
1065
+ this.listData.forEach(row => {
1066
+ this.$set(row, '$v_expanded', true)
1067
+ })
1068
+ }
1069
+ }
1070
+
1071
+ // 外部调动方法 toggleRowExpansion
1072
+ store.toggleRowExpansion = (row, expanded) => {
1073
+ const val = typeof expanded === 'boolean' ? expanded : !row.$v_expanded
1074
+ if (val === Boolean(row.$v_expanded)) return
1075
+ this.$set(row, '$v_expanded', val)
1076
+ // 暂时不做排序,没必要
1077
+ this.elTable.$emit('expand-change', row, this.listData.filter(row => row.$v_expanded))
1078
+
1079
+ if (this.updateExpandeding) return
1080
+ this.updateExpandeding = true
1081
+ this.$nextTick(() => {
1082
+ this.updateExpandeding = false
1083
+ this.update()
1084
+ })
1085
+ }
1086
+
1087
+ // 重写expandRows的indexOf,使其能获取正确的展开状态
1088
+ store.states.expandRows.indexOf = (row) => {
1089
+ if (row) {
1090
+ return row.$v_expanded || -1
1091
+ }
1092
+ return -1
1093
+ }
1094
+ },
1095
+
1096
+ // 【展开行/树】切换某一行的展开状态
1097
+ toggleRowExpansion (row, expanded) {
1098
+ this.elTable.toggleRowExpansion(row, expanded)
1099
+ },
1100
+
1101
+ // 【自定义固定列】设置固定左右样式
1102
+ headerCellFixedStyle (data) {
1103
+ return this.cellFixedStyle(data, true)
1104
+ },
1105
+
1106
+ // 【自定义固定列】设置固定左右样式
1107
+ cellFixedStyle ({ column }, isHeader = false) {
1108
+ const elTable = this.getElTable()
1109
+ if (!elTable) return
1110
+ // 右边固定列头部需要加上滚动条宽度-gutterWidth
1111
+ const { gutterWidth: _gutterWidth, scrollY, bodyWidth } = elTable.layout
1112
+ const gutterWidth = isHeader && scrollY ? _gutterWidth : 0
1113
+ // 计算固定样式(当列宽度变化时重新计算,其余直接使用缓存值fixedMap)
1114
+ if (!this.fixedMap || this._isScrollY !== scrollY || this._bodyWidth !== bodyWidth || this.hasHeadDrag) {
1115
+ if (this.hasHeadDrag) this.hasHeadDrag = false
1116
+ this._isScrollY = scrollY
1117
+ this._bodyWidth = bodyWidth
1118
+ this.fixedMap = {}
1119
+ this.totalLeft = 0 // 左边固定定位累加值
1120
+ this.totalRight = 0 // 右边固定定位累加值
1121
+
1122
+ const columns = elTable.columns
1123
+ const rightColumns = [] // 右边固定列集合
1124
+ let lastLeftColumn // 左边固定列的最后一列
1125
+ let firstRightColumn // 右边固定列的第一列
1126
+ for (let i = 0; i < columns.length; i++) {
1127
+ const column = columns[i]
1128
+ const isLeft = column.className && column.className.includes('virtual-column__fixed-left')
1129
+ const isRight = column.className && column.className.includes('virtual-column__fixed-right')
1130
+
1131
+ if (!isLeft && !isRight) continue
1132
+ // 设置左边固定列定位样式
1133
+ if (isLeft) {
1134
+ lastLeftColumn = column
1135
+ this.fixedMap[column.id] = {
1136
+ left: this.totalLeft
1137
+ }
1138
+ this.totalLeft += column.realWidth || column.width
1139
+ }
1140
+ // 收集右边固定列
1141
+ if (isRight) {
1142
+ if (!firstRightColumn) firstRightColumn = column
1143
+ rightColumns.push(column)
1144
+ }
1145
+ }
1146
+ // 设置固定列阴影classname
1147
+ const leftClass = ' is-last-column'
1148
+ const rightClass = ' is-first-column'
1149
+
1150
+ // 设置左边、右边固定列class
1151
+ if (lastLeftColumn && !lastLeftColumn.className.includes(leftClass)) lastLeftColumn.className += leftClass
1152
+ if (firstRightColumn && !firstRightColumn.className.includes(rightClass)) firstRightColumn.className += rightClass
1153
+
1154
+ // 设置右边固定列定位样式(从右往左依次)
1155
+ this.hasFixedRight = rightColumns.length > 0
1156
+ rightColumns.reverse().forEach(column => {
1157
+ this.fixedMap[column.id] = {
1158
+ right: this.totalRight
1159
+ }
1160
+ this.totalRight += column.realWidth || column.width
1161
+ })
1162
+ }
1163
+ const style = this.fixedMap[column.id]
1164
+ if (!style) return
1165
+ const isFixedRight = 'right' in style
1166
+ const curStyle = isFixedRight ? { right: style.right + gutterWidth + 'px' } : { left: style.left + 'px' }
1167
+
1168
+ // 修复 #89 使用 <virtual-column> vfixed, 表尾合计行对应的列没有固定
1169
+ if (elTable.showSummary) {
1170
+ this.$nextTick(() => {
1171
+ const footTh = this.$el.querySelector(`.el-table__footer-wrapper .${column.id}`)
1172
+ if (footTh) {
1173
+ if (curStyle.left) footTh.style.left = curStyle.left
1174
+ if (curStyle.right) footTh.style.right = curStyle.right
1175
+ }
1176
+ })
1177
+ }
1178
+
1179
+ return curStyle
1180
+ },
1181
+
1182
+ // 【自定义固定列】更新表头布局
1183
+ doHeaderLayout () {
1184
+ if (!this.elTable) return
1185
+ this.fixedMap = null
1186
+ this.elTable.$refs.tableHeader.$forceUpdate()
1187
+ },
1188
+
1189
+ // 绑定排序事件
1190
+ hackTableSort () {
1191
+ this.onSortChange = () => {
1192
+ if (!this.elTable) return
1193
+ const states = this.elTable.store.states
1194
+ const { sortingColumn } = states
1195
+ const data = this.filterData || this.data // 优先使用过滤后的数据进行排序
1196
+ if (!sortingColumn || typeof sortingColumn.sortable === 'string') {
1197
+ this.updateTreeData(data)
1198
+ } else {
1199
+ const listData = orderBy(data, states.sortProp, states.sortOrder, sortingColumn.sortMethod, sortingColumn.sortBy)
1200
+ this.updateTreeData(listData)
1201
+ }
1202
+ // 触发更新
1203
+ this.doUpdate()
1204
+ if (!this.virtualized) {
1205
+ this.renderAllData()
1206
+ }
1207
+ this.$nextTick(() => {
1208
+ this.syncSelectionStatus()
1209
+ })
1210
+ }
1211
+ this.elTable.$on('sort-change', this.onSortChange)
1212
+ this.unWatchs.push(() => {
1213
+ this.elTable.$off('sort-change', this.onSortChange)
1214
+ })
1215
+
1216
+ // clearSort不会触发sort-change,需要重写clearSort方法,手动触发
1217
+ const originClearSort = this.elTable.clearSort.bind(this.elTable)
1218
+ this.elTable.clearSort = (...rest) => {
1219
+ originClearSort(...rest)
1220
+ this.onSortChange()
1221
+ }
1222
+ },
1223
+
1224
+ // 绑定筛选事件
1225
+ hackTableFilter () {
1226
+ // 拦截el-table内部对树的筛选
1227
+ if (this.isTree) {
1228
+ this.$nextTick(() => {
1229
+ this.elTable.store.execQuery = () => {
1230
+ this.elTable.store.states.data = [...this.elTable.data]
1231
+ }
1232
+ })
1233
+ }
1234
+
1235
+ this.onFilterChange = () => {
1236
+ if (!this.elTable) return
1237
+ const states = this.elTable.store.states
1238
+ const { filters } = states
1239
+
1240
+ // 使用原数据进行数据过滤
1241
+ let data = this.data
1242
+ Object.keys(filters).forEach((columnId) => {
1243
+ const values = states.filters[columnId]
1244
+ if (!values || values.length === 0) return
1245
+ const column = getColumnById(states, columnId)
1246
+ if (column && column.filterMethod) {
1247
+ data = data.filter((row) => {
1248
+ return values.some(value => column.filterMethod.call(null, value, row, column))
1249
+ })
1250
+ }
1251
+ })
1252
+
1253
+ // 过滤完之后,手动执行排序
1254
+ const hasFilter = this.data !== data
1255
+ this.filterData = hasFilter ? data : null
1256
+ this.onSortChange()
1257
+ }
1258
+ this.elTable.$on('filter-change', this.onFilterChange)
1259
+ this.unWatchs.push(() => {
1260
+ this.elTable.$off('filter-change', this.onFilterChange)
1261
+ })
1262
+
1263
+ // clearFilter不会触发 filter-change,需要重写 clearFilter 方法,手动触发
1264
+ const originClearFilter = this.elTable.clearFilter.bind(this.elTable)
1265
+ this.elTable.clearFilter = (...rest) => {
1266
+ originClearFilter(...rest)
1267
+ this.onFilterChange()
1268
+ }
1269
+
1270
+ // init filters
1271
+ // 此处兼容列的 filtered-value 属性
1272
+ const states = this.elTable.store.states
1273
+ states.columns.forEach(column => {
1274
+ if (column.filteredValue && column.filteredValue.length) {
1275
+ this.onFilterChange()
1276
+ }
1277
+ })
1278
+ },
1279
+
1280
+ // 使用自定义的树形表格;需禁用原来的树(将children字段改为空)
1281
+ // 场景:row包含children会被当做的树结构,与 virtual-column 的树结构有冲突,所以需要禁用原来的
1282
+ useCustomTree (isTry = true) {
1283
+ this.isTree = true
1284
+ const elTable = this.getElTable()
1285
+ if (elTable) {
1286
+ const states = elTable.store.states
1287
+ states.childrenColumnName = '' // 拦截原来树形表格
1288
+ states.lazyColumnIdentifier = '' // 拦截原来树形懒加载表格
1289
+ elTable.store.updateTreeData = () => {}
1290
+ } else if (isTry) {
1291
+ this.$nextTick(() => {
1292
+ this.useCustomTree(false)
1293
+ })
1294
+ }
1295
+ },
1296
+
1297
+ // 兼容自定义树形表格
1298
+ hackCustomTree () {
1299
+ if (!this.isTree) return
1300
+ // 兼容 el-table 的 expand-row-keys属性
1301
+ const unWatch = this.$watch(() => this.elTable.expandRowKeys, () => {
1302
+ this.expandRowKeysChanged = true
1303
+ this.update()
1304
+ this.expandRowKeysChanged = false
1305
+ })
1306
+ this.unWatchs.push(unWatch)
1307
+
1308
+ // 兼容 el-table 的 toggleRowExpansion
1309
+ this.elTable.toggleRowExpansion = (row, expanded) => {
1310
+ const treeState = row.$v_tree
1311
+ if (!treeState) return
1312
+
1313
+ // 展开状态取反
1314
+ if (typeof expanded === 'undefined') {
1315
+ expanded = !treeState.expanded
1316
+ }
1317
+
1318
+ // 状态一致返回
1319
+ if (treeState.expanded === expanded) return
1320
+ treeState.expanded = expanded
1321
+
1322
+ // 防止多次触发update
1323
+ if (this.togglingRowExpansion) return
1324
+
1325
+ this.togglingRowExpansion = true
1326
+ this.$nextTick(() => {
1327
+ this.togglingRowExpansion = false
1328
+ this.update()
1329
+ this.columnVms.forEach(vm => vm.isTree && vm.renderTreeNode())
1330
+ })
1331
+ }
1332
+ },
1333
+
1334
+ // 更新树:将树结构平铺,将展开的节点筛出来,同时对新节点插入$v_tree 记录树节点状态数据
1335
+ updateTreeData (data = this.data) {
1336
+ if (!this.isTree || !this.treeProps) {
1337
+ this.listData = data
1338
+ return
1339
+ }
1340
+ const res = []
1341
+ const { children = 'children' } = this.treeProps
1342
+ const { defaultExpandAll, expandRowKeys, rowKey, indent } = this.elTable
1343
+
1344
+ const getExpanded = (key) => {
1345
+ if (!key) return false
1346
+ return defaultExpandAll || (expandRowKeys && expandRowKeys.indexOf(key) !== -1) || false
1347
+ }
1348
+
1349
+ const traverse = (nodes, parent, level = 0, display = true) => {
1350
+ nodes.forEach(node => {
1351
+ if (display) {
1352
+ res.push(node)
1353
+ }
1354
+
1355
+ let treeState = node.$v_tree || {}
1356
+ if (!node.$v_tree) {
1357
+ // 如果是新节点,添加 $v_tree($v_tree不设置成响应式,$v_tree值的变更可能会触发data变化,data watch会触发,这是不可控的,会导致updateTreeData多触发一次)
1358
+ node.$v_tree = treeState = {
1359
+ parent,
1360
+ level,
1361
+ expanded: getExpanded(node[rowKey]),
1362
+ loaded: false,
1363
+ loading: false,
1364
+ indent: (level - 1) * indent
1365
+ }
1366
+ } else {
1367
+ // 如果是旧节点
1368
+ // expandRowKeys更改时,旧的节点数据的 expanded(是否展开)以getExpanded方法为准
1369
+ if (this.expandRowKeysChanged) {
1370
+ treeState.expanded = getExpanded(node[rowKey])
1371
+ }
1372
+ }
1373
+
1374
+ if (Array.isArray(node[children])) {
1375
+ // 父节点显示且展开状态时,才显示子节点
1376
+ const childDisplay = display && treeState.expanded
1377
+ traverse(node[children], node, level + 1, childDisplay)
1378
+ }
1379
+ })
1380
+ }
1381
+
1382
+ // 开始遍历树结构
1383
+ traverse(data)
1384
+ this.listData = res
1385
+ // console.log('this.isTree', this.listData, this.listData.map(i => i.id))
1386
+ },
1387
+
1388
+ /*
1389
+ * 【扩展树形表格方法】获取子节点
1390
+ */
1391
+ getChildNodes (row) {
1392
+ const { children = 'children' } = this.treeProps
1393
+ return row[children] || []
1394
+ },
1395
+
1396
+ // 【扩展树形表格方法】获取父节点
1397
+ getParentNode (row) {
1398
+ const treeState = row.$v_tree
1399
+ return treeState && treeState.parent
1400
+ },
1401
+
1402
+ // 【扩展树形表格方法】获取父层所有节点
1403
+ getParentNodes (row) {
1404
+ const res = []
1405
+ let curRow = row
1406
+ while (curRow) {
1407
+ const treeState = curRow.$v_tree
1408
+ if (!treeState || !treeState.parent) break
1409
+ res.unshift(treeState.parent)
1410
+ curRow = treeState.parent
1411
+ }
1412
+ return res
1413
+ },
1414
+
1415
+ // 【扩展树形表格方法】重新加载节点
1416
+ // 删除原来子节点,并触发load函数重新加载
1417
+ async reloadNode (row) {
1418
+ const treeState = row.$v_tree
1419
+ if (!treeState) return
1420
+ const { children = 'children', hasChildren = 'hasChildren' } = this.treeProps
1421
+ row[children] = null
1422
+ row[hasChildren] = true
1423
+ treeState.loaded = false
1424
+ this.update()
1425
+ const vm = this.columnVms.find(vm => vm.isTree)
1426
+ vm && await vm.loadChildNodes(row, true)
1427
+ },
1428
+
1429
+ // 【扩展树形表格方法】收起所有树节点
1430
+ unexpandAllNodes () {
1431
+ const { children } = this.treeProps
1432
+ // 遍历树节点,展开或收起
1433
+ const traverse = (rows) => {
1434
+ rows.forEach(row => {
1435
+ this.toggleRowExpansion(row, false)
1436
+ if (Array.isArray(row[children])) traverse(row[children])
1437
+ })
1438
+ }
1439
+ traverse(this.listData)
1440
+ },
1441
+
1442
+ // 【扩展树形表格方法】展开所有树节点
1443
+ expandAllNodes () {
1444
+ // 遍历树节点,展开或收起
1445
+ const { children, hasChildren = 'hasChildren' } = this.treeProps
1446
+ const traverse = (rows) => {
1447
+ rows.forEach(row => {
1448
+ const treeState = row.$v_tree
1449
+ // 懒加载节点如果未加载则不展开
1450
+ if (row[hasChildren] && !treeState.loaded) return
1451
+ this.toggleRowExpansion(row, true)
1452
+ if (Array.isArray(row[children])) traverse(row[children])
1453
+ })
1454
+ }
1455
+ traverse(this.listData)
1456
+ },
1457
+
1458
+ // 表格销毁事件
1459
+ bindTableDestory () {
1460
+ const onTableDestory = () => {
1461
+ this.warn && console.warn('<el-table> 组件销毁时,建议将 <el-table-virtual-scroll> 组件一同销毁')
1462
+ this.destory()
1463
+ this.$nextTick(() => {
1464
+ this.initData()
1465
+ })
1466
+ }
1467
+ // 防止el-table绑定key时,重新渲染表格但没有重新初始化<virtual-scroll>组件
1468
+ this.elTable.$on('hook:beforeDestory', onTableDestory)
1469
+ this.unWatchs.push(() => {
1470
+ this.elTable.$off('hook:beforeDestory', onTableDestory)
1471
+ })
1472
+ }
1473
+ },
1474
+ watch: {
1475
+ data (data, oldData) {
1476
+ this.listData = data
1477
+
1478
+ if (this.list && data !== oldData) {
1479
+ this.list = data
1480
+ }
1481
+ // 筛选数据后会调用update更新视图
1482
+ this.onFilterChange && this.onFilterChange()
1483
+ if (this.isReserveSelection()) {
1484
+ // 保留旧的选中值
1485
+ this.updateSelectionByRowKey(data, oldData)
1486
+ } else {
1487
+ // 对比新旧值,移除删除的选中值
1488
+ this.updateSelectionData(data, oldData)
1489
+ }
1490
+ },
1491
+ virtualized: {
1492
+ immediate: true,
1493
+ handler () {
1494
+ // 筛选数据后会调用update更新视图
1495
+ this.onFilterChange && this.onFilterChange()
1496
+ }
1497
+ },
1498
+ disabled () {
1499
+ this.doUpdate()
1500
+ },
1501
+ treeMap () {
1502
+ this.update()
1503
+ }
1504
+ },
1505
+ created () {
1506
+ this.$nextTick(() => {
1507
+ this.initData()
1508
+ })
1509
+ },
1510
+ activated () {
1511
+ this.isDeactivated = false
1512
+ if(!this.elTable || (this.elTable && !this.elTable.fit)){
1513
+ this.restoreScroll()
1514
+ }
1515
+ // this.elTable?.fit === false && this.restoreScroll()
1516
+ },
1517
+ deactivated () {
1518
+ this.isDeactivated = true
1519
+ },
1520
+ beforeDestroy () {
1521
+ this.destory()
1522
+ }
1523
+ }
1524
+ </script>
1525
+
1526
+ <style lang="scss">
1527
+ .el-table-virtual-scroll {
1528
+ &.has-custom-fixed-right {
1529
+ .el-table__cell.gutter {
1530
+ position: sticky;
1531
+ right: 0;
1532
+ }
1533
+ }
1534
+ }
1535
+ </style>
1536
+
1537
+ <style lang='scss' scoped>
1538
+ .is-expanding {
1539
+ ::v-deep .el-table__expand-icon {
1540
+ transition: none;
1541
+ }
1542
+ }
1543
+ .hide-append {
1544
+ ::v-deep .el-table__append-wrapper {
1545
+ display: none;
1546
+ }
1547
+ }
1548
+ </style>