stk-table-vue 0.6.12 → 0.6.14

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.
@@ -1,622 +1,622 @@
1
- <template>
2
- <div ref="vScrollTree" class="vtScroll-tree" :style="{ height: height }" @click="$event.stopPropagation()">
3
- <ul
4
- v-if="displayList.length"
5
- :style="{
6
- height: lineHeight * pageSize + 'px',
7
- marginTop: offsetTop + 'px',
8
- marginBottom: offsetBottom > 0 ? offsetBottom + 'px' : 0,
9
- }"
10
- >
11
- <li v-for="item in displayList" :key="item[assignedFields.key]">
12
- <!-- 20: arrow width -->
13
- <div
14
- class="list-item"
15
- :class="{
16
- 'item-parent': item._vt_isParent,
17
- 'item-current': item[assignedFields.key] === currentItem[assignedFields.key],
18
- 'item-highlight': highlightCurrent && item[assignedFields.key] === currentItem[assignedFields.key],
19
- }"
20
- :style="{
21
- height: lineHeight + 'px',
22
- paddingLeft: item._vt_isParent
23
- ? baseIndentWidth + indentWidth * item._vt_level + 'px'
24
- : baseIndentWidth + indentWidth * (item._vt_level - 1) + 20 + 'px',
25
- }"
26
- @click="handleItemClick(item, isClick)"
27
- @dblclick="onItemDblClick(item)"
28
- @contextmenu="e => onContextMenu(e, item)"
29
- >
30
- <!-- 展开箭头 -->
31
- <div v-if="item._vt_isParent" class="list-item-expand" @click.stop="changeList(item)">
32
- <!-- slot 箭头 -->
33
- <slot name="icon" :is-expand="item._vt_isExpand">
34
- <div class="list-item-arrow" :class="{ 'list-item-arrow-active': item._vt_isExpand }"></div>
35
- </slot>
36
- </div>
37
- <!-- 多选框 -->
38
- <div v-if="showCheckbox">
39
- <input
40
- :checked="selectedItems.includes(item)"
41
- type="checkbox"
42
- @click="onCheckboxClick"
43
- @change="onCheckboxChange($event, item)"
44
- />
45
- </div>
46
- <!-- 文字 -->
47
- <div class="list-item-title" :title="item[assignedFields.title]">
48
- <!-- 文字slot -->
49
- <slot name="text" :text="item[assignedFields.title]">
50
- <span>{{ item[assignedFields.title] }}</span>
51
- </slot>
52
- </div>
53
- </div>
54
- </li>
55
- </ul>
56
- <div v-else class="vtScroll-empty">{{ emptyText }}</div>
57
- </div>
58
- </template>
59
-
60
- <script>
61
- const _defaultFields = {
62
- key: 'key',
63
- title: 'title',
64
- children: 'children',
65
- };
66
- export default {
67
- name: 'VirtualTree',
68
- props: {
69
- /** 树高度 默认auto */
70
- height: {
71
- type: String,
72
- default: 'auto',
73
- },
74
- /** 行高 */
75
- lineHeight: {
76
- type: Number,
77
- default: 30,
78
- },
79
- /** 基础缩进距离 */
80
- baseIndentWidth: {
81
- type: Number,
82
- default: 4,
83
- },
84
- /** 每个层级的缩进距离 */
85
- indentWidth: {
86
- type: Number,
87
- default: 20,
88
- },
89
- /** 展示checkbox,可多选 */
90
- showCheckbox: {
91
- type: Boolean,
92
- default: false,
93
- },
94
- /**
95
- * @deprecated
96
- * 是否支持多选
97
- * 单选用this.currentItem 高亮行
98
- * 多选用selectedItems 开启多选框来支持
99
- */
100
- multiple: {
101
- type: Boolean,
102
- default: false,
103
- },
104
- /**
105
- * 点击一项时,是否设置currentItem。
106
- * 设置false,一般用于this.setCurrent 手动指定当前选中项
107
- */
108
- setCurrentWhenClick: {
109
- type: Boolean,
110
- default: true,
111
- },
112
- /** 高亮选中行 */
113
- highlightCurrent: {
114
- type: Boolean,
115
- default: true,
116
- },
117
- /** 当前行是否可取消 */
118
- currentCancelable: {
119
- type: Boolean,
120
- default: false,
121
- },
122
- /** 父节点是否可点击为current(是否可选中) */
123
- parentSelectable: {
124
- type: Boolean,
125
- default: false,
126
- },
127
- /** 点击一项也可以展开,而非只能点击箭头*/
128
- clickItemExpand: {
129
- type: Boolean,
130
- default: false,
131
- },
132
- /** 无数据时显示的内容 */
133
- emptyText: {
134
- type: String,
135
- default: '暂无数据',
136
- },
137
- /** 数据,出于性能考虑,不进行深拷贝复制一份维护 */
138
- treeData: {
139
- type: Array,
140
- default: () => [],
141
- },
142
- /** 默认展开的键数组 (区分Number、String类型) */
143
- defaultExpandedKeys: {
144
- type: Array,
145
- default: () => [],
146
- },
147
- /** 默认高亮的当前行key */
148
- defaultCurrentKey: {
149
- type: String,
150
- default: '',
151
- },
152
- /**
153
- * 滚动条默认的位置的key
154
- * 如果有父节点,则父节点必须要展开,否则不会定位
155
- */
156
- defaultScrollKey: {
157
- type: String,
158
- default: '',
159
- },
160
- /** 默认选中的项数组 */
161
- defaultSelectedKeys: {
162
- type: Array,
163
- default: () => [],
164
- },
165
- /** 默认展开所有节点 */
166
- defaultExpandAll: {
167
- type: Boolean,
168
- default: false,
169
- },
170
- /** 替换数据title,key,children字段 */
171
- replaceFields: {
172
- type: Object,
173
- default: () => _defaultFields,
174
- },
175
- },
176
- data() {
177
- return {
178
- /** window resize debounce */
179
- resizeTimeout: null,
180
- // rootEl: null, // 根元素
181
- /** 展平的一维数组 */
182
- treeDataFlat: [],
183
- /** 多选选中*/
184
- selectedItems: [],
185
- // var
186
- /** 点击后高亮的行 */
187
- currentItem: {},
188
- // v scroll
189
- startIndex: 0,
190
- endIndex: 30,
191
- offsetTop: 0,
192
- offsetBottom: 0,
193
- pageSize: 30,
194
- };
195
- },
196
- computed: {
197
- /** 合并传入的fields */
198
- assignedFields() {
199
- return Object.assign({}, _defaultFields, this.replaceFields);
200
- },
201
- /** 实际显示的列表*/
202
- displayList() {
203
- return this.treeDataFlat.slice(this.startIndex, this.endIndex);
204
- },
205
- /** 总高度 */
206
- allHeight() {
207
- return this.treeDataFlat.length * this.lineHeight;
208
- },
209
- /** 活动页面高度 */
210
- mainPageHeight() {
211
- return this.pageSize * this.lineHeight;
212
- },
213
- },
214
- watch: {
215
- treeData() {
216
- // 列表发生改变,重置已选,重置虚拟滚动
217
- this.init();
218
- this.setDefaultCurrent(); // 设置默认高亮行
219
- this.scrollTo(); // 滚动条默认的位置
220
- this.setDefaultSelected(); // 设置默认选中
221
- },
222
- },
223
- mounted() {
224
- this.init();
225
- this.setDefaultCurrent(); // 设置默认高亮行
226
- this.scrollTo(); // 滚动条默认的位置
227
- this.setDefaultSelected(); // 设置默认选中
228
- // event listener
229
- this.initEvent();
230
- },
231
- methods: {
232
- /**
233
- * @param {"init"|"resize"} [type="init"]
234
- */
235
- init(type = 'init') {
236
- let containerHeight = this.$el?.clientHeight;
237
- if (!containerHeight) {
238
- containerHeight = 1080;
239
- console.warn("Can't get virtualTree clientHeight");
240
- }
241
- if (type === 'init') {
242
- this.initTreeDataPrivateProp();
243
- }
244
- // console.log('Tree containerHeight:', containerHeight);
245
- this.setTreeDataFlat(type); // 默认展开树,获得总高度 allHeight
246
- this.pageSize = Math.ceil(containerHeight / this.lineHeight) + 1;
247
- this.setIndex();
248
-
249
- this.selectedItems = [];
250
- },
251
- initEvent() {
252
- this.$el.addEventListener('scroll', this.setIndex);
253
- window.addEventListener('resize', () => {
254
- this.resize();
255
- });
256
- },
257
- /** 向treeData中添加私有属性 */
258
- initTreeDataPrivateProp() {
259
- // level 树层级
260
- (function func(arr, level = 0, parent) {
261
- arr.forEach(item => {
262
- item._vt_isParent = Boolean(item[this.assignedFields.children]);
263
- item._vt_level = level;
264
- item._vt_parent = parent; // 持有父节点引用
265
- item._vt_isExpand = false; // 是否展开
266
- if (item._vt_isParent) {
267
- func.bind(this)(item[this.assignedFields.children] || [], level + 1, item);
268
- }
269
- });
270
- }).bind(this)(this.treeData, 0, null);
271
- },
272
-
273
- /** 设置默认高亮当前行 (仅单选)*/
274
- setDefaultCurrent() {
275
- this.traverseTreeData(item => {
276
- if (!this.defaultCurrentKey) return;
277
- const defaultKey = this.defaultCurrentKey;
278
- if (item[this.assignedFields.key] === defaultKey) {
279
- this.currentItem = item;
280
- return 0;
281
- }
282
- });
283
- },
284
- /** 设置选中的项 (可多选)*/
285
- setDefaultSelected() {
286
- if (!this.defaultSelectedKeys?.length) return;
287
- this.traverseTreeData(item => {
288
- if (this.defaultSelectedKeys.includes(item[this.assignedFields.key])) {
289
- this.selectedItems.push(item);
290
- if (this.selectedItems.length === this.defaultSelectedKeys.length) return 0;
291
- }
292
- });
293
- },
294
- /**
295
- * 设置当前展开数组
296
- * @param {String} type 'init'
297
- */
298
- setTreeDataFlat(type) {
299
- const treeDataFlat = [];
300
- // level 树层级
301
- (function func(arr) {
302
- arr.forEach(item => {
303
- treeDataFlat.push(item);
304
- if (type === 'init') {
305
- item._vt_isExpand = this.defaultExpandAll ? true : this.defaultExpandedKeys.includes(item[this.assignedFields.key]);
306
- }
307
- if (item._vt_isExpand) {
308
- func.bind(this)(item[this.assignedFields.children] || []);
309
- }
310
- });
311
- }).bind(this)(this.treeData);
312
-
313
- this.treeDataFlat = treeDataFlat;
314
- },
315
- /** 展开收起事件回调 */
316
- changeList(item) {
317
- this.offsetBottom = 0;
318
- this.offsetTop = 0;
319
- // this.$set(item, '_vt_isExpand', !item._vt_isExpand);
320
- item._vt_isExpand = !item._vt_isExpand;
321
- // 若当前节点选中,则展开时清空子节点选中
322
- this.setTreeDataFlat();
323
- this.offsetTop = this.startIndex * this.lineHeight;
324
- this.offsetBottom = this.allHeight - (this.displayList.length + this.startIndex) * this.lineHeight;
325
- },
326
- /**
327
- * 根据滚动条位置,设置展示的区间
328
- * 不传参数则默认获取$el的scrollTop
329
- * @param {MouseEvent} e default this.$el.scrollTop
330
- */
331
- setIndex(e) {
332
- const top = e ? e.target.scrollTop : this.$el?.scrollTop;
333
- this.startIndex = Math.floor(top / this.lineHeight);
334
- const offset = top % this.lineHeight; // 半行偏移量
335
- this.offsetTop = top - offset;
336
- this.endIndex = this.startIndex + this.pageSize;
337
-
338
- this.offsetBottom = this.allHeight - this.mainPageHeight - this.offsetTop;
339
- },
340
- /**
341
- * 点击一项
342
- * @param {object} item
343
- * @param {boolean} isClick 是否点击列表触发
344
- */
345
- handleItemClick(item, isClick = false) {
346
- if (this.clickItemExpand && item[this.assignedFields.children]) {
347
- this.changeList(item); // 展开
348
- }
349
- if (!this.parentSelectable) {
350
- // 父节点不可选中
351
- if (item[this.assignedFields.children]) return;
352
- }
353
- if (this.setCurrentWhenClick) {
354
- if (this.currentCancelable) {
355
- this.currentItem = this.currentItem === item ? {} : item;
356
- } else {
357
- this.currentItem = item;
358
- }
359
- }
360
- this.$emit('item-click', item, isClick);
361
- // this.setSelectedItem(item);
362
- },
363
- /** 设置选中项 */
364
- setSelectedItem(item, checked) {
365
- if (checked) {
366
- this.selectedItems.push(item);
367
- } else {
368
- const i = this.selectedItems.indexOf(item);
369
- if (i > -1) {
370
- this.selectedItems.splice(i, 1); // FIXME: 数据量大有性能问题?
371
- }
372
- }
373
- this.$emit('item-select', {
374
- checked,
375
- item,
376
- selectedItems: this.selectedItems,
377
- });
378
- },
379
- /** 双击一项 */
380
- onItemDblClick(item) {
381
- // if (item[this.assignedFields.children]) {
382
- // // 展开父节点
383
- // this.changeList(item);
384
- // } else {
385
- // // 选中节点
386
- // // this.setSelectedItem(item);
387
- // // item.isCurrent = true; // this.$set(item, 'isCurrent', true);
388
- // }
389
- // if (!this.parentSelectable) {
390
- // if (item[this.assignedFields.children]) return;
391
- // }
392
- this.$emit('item-dblclick', item);
393
- },
394
- onContextMenu(e, item) {
395
- this.$emit('right-click', { event: e, item });
396
- },
397
- onCheckboxChange(e, item) {
398
- this.setSelectedItem(item, e.target.checked);
399
- },
400
- onCheckboxClick(e) {
401
- e.stopPropagation();
402
- // TODO: if parent checked ,check children
403
- },
404
-
405
- // ---------- utils
406
- /**
407
- * 遍历treeData方法
408
- * @return 0 跳出循环
409
- */
410
- traverseTreeData(callback) {
411
- (function recursion(arr) {
412
- for (let i = 0; i < arr.length; i++) {
413
- const item = arr[i];
414
- const cbRes = callback(item, i);
415
- if (cbRes === 0) return 0;
416
- if (item[this.assignedFields.children]) {
417
- const res = recursion.bind(this)(item[this.assignedFields.children]);
418
- if (res === 0) return 0;
419
- }
420
- }
421
- }).bind(this)(this.treeData);
422
- },
423
-
424
- // ------ ref Func------
425
- /** 清除当前选中的高亮 */
426
- clearCurrent() {
427
- this.currentItem = {};
428
- },
429
- /**
430
- * 重新初始化并计算大小
431
- * @param {number} options.debounce
432
- */
433
- resize(options = {}) {
434
- // debounce
435
- if (this.resizeTimeout) clearTimeout(this.resizeTimeout);
436
- this.resizeTimeout = setTimeout(() => {
437
- this.init('resize');
438
- this.resizeTimeout = null;
439
- }, options.debounce || 200);
440
- },
441
- /**
442
- * 设置当前选中行
443
- * @param {string | object} key 选中行的唯一键,或者传入一个对象
444
- * @return {object} currentItem 当前选中的项
445
- */
446
- setCurrent(key) {
447
- if (typeof key !== 'object') {
448
- this.traverseTreeData(item => {
449
- if (item[this.assignedFields.key] === key) {
450
- this.currentItem = item;
451
- return 0;
452
- }
453
- });
454
- } else {
455
- this.currentItem = key;
456
- }
457
- return this.currentItem;
458
- },
459
- /**
460
- * 展开行
461
- * @param {string[]} keys
462
- * @param {boolean} options.expandParent 是否展开其父节点
463
- * @param {boolean} options.foldOthers 是否折叠其他父节点
464
- */
465
- expandItem(keys, options = {}) {
466
- options = Object.assign({ expandParent: true, foldOthers: false }, options);
467
-
468
- if (!keys?.length) {
469
- if (Object.keys(this.currentItem).length) {
470
- keys = [this.currentItem];
471
- } else {
472
- throw new Error('vScrollTree.expandItem Error: keys is empty');
473
- }
474
- }
475
- // 需要展开的项的数量
476
- let itemsLen = 0;
477
- this.traverseTreeData(item => {
478
- if (options.foldOthers) {
479
- item._vt_isExpand = false;
480
- }
481
- if (keys.includes(item[this.assignedFields.key])) {
482
- // 找到该项,设置为展开
483
- item._vt_isExpand = true;
484
- if (options.expandParent) {
485
- // 展开其所有父节点
486
- while (item._vt_parent) {
487
- item = item._vt_parent;
488
- item._vt_isExpand = true;
489
- }
490
- }
491
- if (!options.foldOthers) {
492
- itemsLen++;
493
- if (itemsLen >= keys.length) {
494
- // 已经找到所有,终止遍历
495
- return 0;
496
- }
497
- }
498
- }
499
- });
500
-
501
- // 更新展开数组
502
- this.setTreeDataFlat();
503
- },
504
- /**
505
- * 滚动到某一项,此项必须已经展开可见
506
- * 默认滚动到defaultScrollKey
507
- * @param {string} key
508
- */
509
- scrollTo(key = this.defaultScrollKey) {
510
- this.$nextTick(() => {
511
- this.$el.scrollTop = this.treeDataFlat.findIndex(it => it[this.assignedFields.key] === key) * this.lineHeight;
512
- });
513
- },
514
- },
515
- };
516
- </script>
517
-
518
- <style scoped lang="less">
519
- .vtScroll-tree {
520
- box-sizing: border-box;
521
- background-color: #fff;
522
- user-select: none;
523
- width: 100%;
524
- height: 100%;
525
- // min-height: 300px;
526
- display: flex;
527
- flex-direction: column;
528
- overflow: auto;
529
- overflow: overlay;
530
- &::-webkit-scrollbar {
531
- width: 6px;
532
- height: 6px;
533
- }
534
- &::-webkit-scrollbar-track {
535
- border-radius: 5px;
536
- }
537
- &::-webkit-scrollbar-thumb {
538
- background: rgba(74, 75, 114, 0.4);
539
- border-radius: 5px;
540
- &:hover {
541
- background: rgba(74, 75, 114, 0.6);
542
- }
543
- }
544
- &::-webkit-scrollbar-corner {
545
- background: transparent;
546
- }
547
- ul {
548
- height: 100%;
549
- padding: 0;
550
- margin: 0;
551
- flex: 1;
552
- // width: max-content;
553
- width: 100%;
554
- min-width: max-content;
555
-
556
- li {
557
- width: 100%;
558
- min-width: max-content;
559
- list-style-type: none;
560
- .list-item {
561
- box-sizing: border-box; //确保padding-right在min-width: 100%;里
562
- color: #000;
563
- min-width: 100%;
564
- width: max-content;
565
- display: flex;
566
- padding-right: 10px;
567
- cursor: pointer;
568
- align-items: center;
569
- &.item-current {
570
- }
571
- &.item-highlight {
572
- color: #fff;
573
- background-color: #1b63d9;
574
- .list-item-expand .list-item-arrow {
575
- border-left: 5px solid #fff;
576
- }
577
- }
578
- &:hover:not(.item-highlight) {
579
- background-color: #eee;
580
- }
581
-
582
- // 父节点
583
- &.item-parent {
584
- font-weight: bold;
585
- }
586
- .list-item-expand {
587
- height: 16px;
588
- width: 16px;
589
- display: flex;
590
- align-items: center;
591
- justify-content: center;
592
- &:hover {
593
- opacity: 0.6;
594
- }
595
- /*箭头 */
596
- .list-item-arrow {
597
- transform-origin: 2px center;
598
- border-left: 5px solid #757699; // color 继承自祖先元素
599
- border-top: 4.5px solid transparent;
600
- border-bottom: 4.5px solid transparent;
601
- border-right: 0px;
602
- transition: transform 0.2s ease;
603
- &.list-item-arrow-active {
604
- transform: rotate(90deg);
605
- }
606
- }
607
- }
608
- .list-item-title {
609
- margin-left: 5px;
610
- white-space: nowrap;
611
- }
612
- }
613
- }
614
- }
615
- .vtScroll-empty {
616
- height: 100%;
617
- display: flex;
618
- align-items: center;
619
- justify-content: center;
620
- }
621
- }
622
- </style>
1
+ <template>
2
+ <div ref="vScrollTree" class="vtScroll-tree" :style="{ height: height }" @click="$event.stopPropagation()">
3
+ <ul
4
+ v-if="displayList.length"
5
+ :style="{
6
+ height: lineHeight * pageSize + 'px',
7
+ marginTop: offsetTop + 'px',
8
+ marginBottom: offsetBottom > 0 ? offsetBottom + 'px' : 0,
9
+ }"
10
+ >
11
+ <li v-for="item in displayList" :key="item[assignedFields.key]">
12
+ <!-- 20: arrow width -->
13
+ <div
14
+ class="list-item"
15
+ :class="{
16
+ 'item-parent': item._vt_isParent,
17
+ 'item-current': item[assignedFields.key] === currentItem[assignedFields.key],
18
+ 'item-highlight': highlightCurrent && item[assignedFields.key] === currentItem[assignedFields.key],
19
+ }"
20
+ :style="{
21
+ height: lineHeight + 'px',
22
+ paddingLeft: item._vt_isParent
23
+ ? baseIndentWidth + indentWidth * item._vt_level + 'px'
24
+ : baseIndentWidth + indentWidth * (item._vt_level - 1) + 20 + 'px',
25
+ }"
26
+ @click="handleItemClick(item, isClick)"
27
+ @dblclick="onItemDblClick(item)"
28
+ @contextmenu="e => onContextMenu(e, item)"
29
+ >
30
+ <!-- 展开箭头 -->
31
+ <div v-if="item._vt_isParent" class="list-item-expand" @click.stop="changeList(item)">
32
+ <!-- slot 箭头 -->
33
+ <slot name="icon" :is-expand="item._vt_isExpand">
34
+ <div class="list-item-arrow" :class="{ 'list-item-arrow-active': item._vt_isExpand }"></div>
35
+ </slot>
36
+ </div>
37
+ <!-- 多选框 -->
38
+ <div v-if="showCheckbox">
39
+ <input
40
+ :checked="selectedItems.includes(item)"
41
+ type="checkbox"
42
+ @click="onCheckboxClick"
43
+ @change="onCheckboxChange($event, item)"
44
+ />
45
+ </div>
46
+ <!-- 文字 -->
47
+ <div class="list-item-title" :title="item[assignedFields.title]">
48
+ <!-- 文字slot -->
49
+ <slot name="text" :text="item[assignedFields.title]">
50
+ <span>{{ item[assignedFields.title] }}</span>
51
+ </slot>
52
+ </div>
53
+ </div>
54
+ </li>
55
+ </ul>
56
+ <div v-else class="vtScroll-empty">{{ emptyText }}</div>
57
+ </div>
58
+ </template>
59
+
60
+ <script>
61
+ const _defaultFields = {
62
+ key: 'key',
63
+ title: 'title',
64
+ children: 'children',
65
+ };
66
+ export default {
67
+ name: 'VirtualTree',
68
+ props: {
69
+ /** 树高度 默认auto */
70
+ height: {
71
+ type: String,
72
+ default: 'auto',
73
+ },
74
+ /** 行高 */
75
+ lineHeight: {
76
+ type: Number,
77
+ default: 30,
78
+ },
79
+ /** 基础缩进距离 */
80
+ baseIndentWidth: {
81
+ type: Number,
82
+ default: 4,
83
+ },
84
+ /** 每个层级的缩进距离 */
85
+ indentWidth: {
86
+ type: Number,
87
+ default: 20,
88
+ },
89
+ /** 展示checkbox,可多选 */
90
+ showCheckbox: {
91
+ type: Boolean,
92
+ default: false,
93
+ },
94
+ /**
95
+ * @deprecated
96
+ * 是否支持多选
97
+ * 单选用this.currentItem 高亮行
98
+ * 多选用selectedItems 开启多选框来支持
99
+ */
100
+ multiple: {
101
+ type: Boolean,
102
+ default: false,
103
+ },
104
+ /**
105
+ * 点击一项时,是否设置currentItem。
106
+ * 设置false,一般用于this.setCurrent 手动指定当前选中项
107
+ */
108
+ setCurrentWhenClick: {
109
+ type: Boolean,
110
+ default: true,
111
+ },
112
+ /** 高亮选中行 */
113
+ highlightCurrent: {
114
+ type: Boolean,
115
+ default: true,
116
+ },
117
+ /** 当前行是否可取消 */
118
+ currentCancelable: {
119
+ type: Boolean,
120
+ default: false,
121
+ },
122
+ /** 父节点是否可点击为current(是否可选中) */
123
+ parentSelectable: {
124
+ type: Boolean,
125
+ default: false,
126
+ },
127
+ /** 点击一项也可以展开,而非只能点击箭头*/
128
+ clickItemExpand: {
129
+ type: Boolean,
130
+ default: false,
131
+ },
132
+ /** 无数据时显示的内容 */
133
+ emptyText: {
134
+ type: String,
135
+ default: '暂无数据',
136
+ },
137
+ /** 数据,出于性能考虑,不进行深拷贝复制一份维护 */
138
+ treeData: {
139
+ type: Array,
140
+ default: () => [],
141
+ },
142
+ /** 默认展开的键数组 (区分Number、String类型) */
143
+ defaultExpandedKeys: {
144
+ type: Array,
145
+ default: () => [],
146
+ },
147
+ /** 默认高亮的当前行key */
148
+ defaultCurrentKey: {
149
+ type: String,
150
+ default: '',
151
+ },
152
+ /**
153
+ * 滚动条默认的位置的key
154
+ * 如果有父节点,则父节点必须要展开,否则不会定位
155
+ */
156
+ defaultScrollKey: {
157
+ type: String,
158
+ default: '',
159
+ },
160
+ /** 默认选中的项数组 */
161
+ defaultSelectedKeys: {
162
+ type: Array,
163
+ default: () => [],
164
+ },
165
+ /** 默认展开所有节点 */
166
+ defaultExpandAll: {
167
+ type: Boolean,
168
+ default: false,
169
+ },
170
+ /** 替换数据title,key,children字段 */
171
+ replaceFields: {
172
+ type: Object,
173
+ default: () => _defaultFields,
174
+ },
175
+ },
176
+ data() {
177
+ return {
178
+ /** window resize debounce */
179
+ resizeTimeout: null,
180
+ // rootEl: null, // 根元素
181
+ /** 展平的一维数组 */
182
+ treeDataFlat: [],
183
+ /** 多选选中*/
184
+ selectedItems: [],
185
+ // var
186
+ /** 点击后高亮的行 */
187
+ currentItem: {},
188
+ // v scroll
189
+ startIndex: 0,
190
+ endIndex: 30,
191
+ offsetTop: 0,
192
+ offsetBottom: 0,
193
+ pageSize: 30,
194
+ };
195
+ },
196
+ computed: {
197
+ /** 合并传入的fields */
198
+ assignedFields() {
199
+ return Object.assign({}, _defaultFields, this.replaceFields);
200
+ },
201
+ /** 实际显示的列表*/
202
+ displayList() {
203
+ return this.treeDataFlat.slice(this.startIndex, this.endIndex);
204
+ },
205
+ /** 总高度 */
206
+ allHeight() {
207
+ return this.treeDataFlat.length * this.lineHeight;
208
+ },
209
+ /** 活动页面高度 */
210
+ mainPageHeight() {
211
+ return this.pageSize * this.lineHeight;
212
+ },
213
+ },
214
+ watch: {
215
+ treeData() {
216
+ // 列表发生改变,重置已选,重置虚拟滚动
217
+ this.init();
218
+ this.setDefaultCurrent(); // 设置默认高亮行
219
+ this.scrollTo(); // 滚动条默认的位置
220
+ this.setDefaultSelected(); // 设置默认选中
221
+ },
222
+ },
223
+ mounted() {
224
+ this.init();
225
+ this.setDefaultCurrent(); // 设置默认高亮行
226
+ this.scrollTo(); // 滚动条默认的位置
227
+ this.setDefaultSelected(); // 设置默认选中
228
+ // event listener
229
+ this.initEvent();
230
+ },
231
+ methods: {
232
+ /**
233
+ * @param {"init"|"resize"} [type="init"]
234
+ */
235
+ init(type = 'init') {
236
+ let containerHeight = this.$el?.clientHeight;
237
+ if (!containerHeight) {
238
+ containerHeight = 1080;
239
+ console.warn("Can't get virtualTree clientHeight");
240
+ }
241
+ if (type === 'init') {
242
+ this.initTreeDataPrivateProp();
243
+ }
244
+ // console.log('Tree containerHeight:', containerHeight);
245
+ this.setTreeDataFlat(type); // 默认展开树,获得总高度 allHeight
246
+ this.pageSize = Math.ceil(containerHeight / this.lineHeight) + 1;
247
+ this.setIndex();
248
+
249
+ this.selectedItems = [];
250
+ },
251
+ initEvent() {
252
+ this.$el.addEventListener('scroll', this.setIndex);
253
+ window.addEventListener('resize', () => {
254
+ this.resize();
255
+ });
256
+ },
257
+ /** 向treeData中添加私有属性 */
258
+ initTreeDataPrivateProp() {
259
+ // level 树层级
260
+ (function func(arr, level = 0, parent) {
261
+ arr.forEach(item => {
262
+ item._vt_isParent = Boolean(item[this.assignedFields.children]);
263
+ item._vt_level = level;
264
+ item._vt_parent = parent; // 持有父节点引用
265
+ item._vt_isExpand = false; // 是否展开
266
+ if (item._vt_isParent) {
267
+ func.bind(this)(item[this.assignedFields.children] || [], level + 1, item);
268
+ }
269
+ });
270
+ }).bind(this)(this.treeData, 0, null);
271
+ },
272
+
273
+ /** 设置默认高亮当前行 (仅单选)*/
274
+ setDefaultCurrent() {
275
+ this.traverseTreeData(item => {
276
+ if (!this.defaultCurrentKey) return;
277
+ const defaultKey = this.defaultCurrentKey;
278
+ if (item[this.assignedFields.key] === defaultKey) {
279
+ this.currentItem = item;
280
+ return 0;
281
+ }
282
+ });
283
+ },
284
+ /** 设置选中的项 (可多选)*/
285
+ setDefaultSelected() {
286
+ if (!this.defaultSelectedKeys?.length) return;
287
+ this.traverseTreeData(item => {
288
+ if (this.defaultSelectedKeys.includes(item[this.assignedFields.key])) {
289
+ this.selectedItems.push(item);
290
+ if (this.selectedItems.length === this.defaultSelectedKeys.length) return 0;
291
+ }
292
+ });
293
+ },
294
+ /**
295
+ * 设置当前展开数组
296
+ * @param {String} type 'init'
297
+ */
298
+ setTreeDataFlat(type) {
299
+ const treeDataFlat = [];
300
+ // level 树层级
301
+ (function func(arr) {
302
+ arr.forEach(item => {
303
+ treeDataFlat.push(item);
304
+ if (type === 'init') {
305
+ item._vt_isExpand = this.defaultExpandAll ? true : this.defaultExpandedKeys.includes(item[this.assignedFields.key]);
306
+ }
307
+ if (item._vt_isExpand) {
308
+ func.bind(this)(item[this.assignedFields.children] || []);
309
+ }
310
+ });
311
+ }).bind(this)(this.treeData);
312
+
313
+ this.treeDataFlat = treeDataFlat;
314
+ },
315
+ /** 展开收起事件回调 */
316
+ changeList(item) {
317
+ this.offsetBottom = 0;
318
+ this.offsetTop = 0;
319
+ // this.$set(item, '_vt_isExpand', !item._vt_isExpand);
320
+ item._vt_isExpand = !item._vt_isExpand;
321
+ // 若当前节点选中,则展开时清空子节点选中
322
+ this.setTreeDataFlat();
323
+ this.offsetTop = this.startIndex * this.lineHeight;
324
+ this.offsetBottom = this.allHeight - (this.displayList.length + this.startIndex) * this.lineHeight;
325
+ },
326
+ /**
327
+ * 根据滚动条位置,设置展示的区间
328
+ * 不传参数则默认获取$el的scrollTop
329
+ * @param {MouseEvent} e default this.$el.scrollTop
330
+ */
331
+ setIndex(e) {
332
+ const top = e ? e.target.scrollTop : this.$el?.scrollTop;
333
+ this.startIndex = Math.floor(top / this.lineHeight);
334
+ const offset = top % this.lineHeight; // 半行偏移量
335
+ this.offsetTop = top - offset;
336
+ this.endIndex = this.startIndex + this.pageSize;
337
+
338
+ this.offsetBottom = this.allHeight - this.mainPageHeight - this.offsetTop;
339
+ },
340
+ /**
341
+ * 点击一项
342
+ * @param {object} item
343
+ * @param {boolean} isClick 是否点击列表触发
344
+ */
345
+ handleItemClick(item, isClick = false) {
346
+ if (this.clickItemExpand && item[this.assignedFields.children]) {
347
+ this.changeList(item); // 展开
348
+ }
349
+ if (!this.parentSelectable) {
350
+ // 父节点不可选中
351
+ if (item[this.assignedFields.children]) return;
352
+ }
353
+ if (this.setCurrentWhenClick) {
354
+ if (this.currentCancelable) {
355
+ this.currentItem = this.currentItem === item ? {} : item;
356
+ } else {
357
+ this.currentItem = item;
358
+ }
359
+ }
360
+ this.$emit('item-click', item, isClick);
361
+ // this.setSelectedItem(item);
362
+ },
363
+ /** 设置选中项 */
364
+ setSelectedItem(item, checked) {
365
+ if (checked) {
366
+ this.selectedItems.push(item);
367
+ } else {
368
+ const i = this.selectedItems.indexOf(item);
369
+ if (i > -1) {
370
+ this.selectedItems.splice(i, 1); // FIXME: 数据量大有性能问题?
371
+ }
372
+ }
373
+ this.$emit('item-select', {
374
+ checked,
375
+ item,
376
+ selectedItems: this.selectedItems,
377
+ });
378
+ },
379
+ /** 双击一项 */
380
+ onItemDblClick(item) {
381
+ // if (item[this.assignedFields.children]) {
382
+ // // 展开父节点
383
+ // this.changeList(item);
384
+ // } else {
385
+ // // 选中节点
386
+ // // this.setSelectedItem(item);
387
+ // // item.isCurrent = true; // this.$set(item, 'isCurrent', true);
388
+ // }
389
+ // if (!this.parentSelectable) {
390
+ // if (item[this.assignedFields.children]) return;
391
+ // }
392
+ this.$emit('item-dblclick', item);
393
+ },
394
+ onContextMenu(e, item) {
395
+ this.$emit('right-click', { event: e, item });
396
+ },
397
+ onCheckboxChange(e, item) {
398
+ this.setSelectedItem(item, e.target.checked);
399
+ },
400
+ onCheckboxClick(e) {
401
+ e.stopPropagation();
402
+ // TODO: if parent checked ,check children
403
+ },
404
+
405
+ // ---------- utils
406
+ /**
407
+ * 遍历treeData方法
408
+ * @return 0 跳出循环
409
+ */
410
+ traverseTreeData(callback) {
411
+ (function recursion(arr) {
412
+ for (let i = 0; i < arr.length; i++) {
413
+ const item = arr[i];
414
+ const cbRes = callback(item, i);
415
+ if (cbRes === 0) return 0;
416
+ if (item[this.assignedFields.children]) {
417
+ const res = recursion.bind(this)(item[this.assignedFields.children]);
418
+ if (res === 0) return 0;
419
+ }
420
+ }
421
+ }).bind(this)(this.treeData);
422
+ },
423
+
424
+ // ------ ref Func------
425
+ /** 清除当前选中的高亮 */
426
+ clearCurrent() {
427
+ this.currentItem = {};
428
+ },
429
+ /**
430
+ * 重新初始化并计算大小
431
+ * @param {number} options.debounce
432
+ */
433
+ resize(options = {}) {
434
+ // debounce
435
+ if (this.resizeTimeout) clearTimeout(this.resizeTimeout);
436
+ this.resizeTimeout = setTimeout(() => {
437
+ this.init('resize');
438
+ this.resizeTimeout = null;
439
+ }, options.debounce || 200);
440
+ },
441
+ /**
442
+ * 设置当前选中行
443
+ * @param {string | object} key 选中行的唯一键,或者传入一个对象
444
+ * @return {object} currentItem 当前选中的项
445
+ */
446
+ setCurrent(key) {
447
+ if (typeof key !== 'object') {
448
+ this.traverseTreeData(item => {
449
+ if (item[this.assignedFields.key] === key) {
450
+ this.currentItem = item;
451
+ return 0;
452
+ }
453
+ });
454
+ } else {
455
+ this.currentItem = key;
456
+ }
457
+ return this.currentItem;
458
+ },
459
+ /**
460
+ * 展开行
461
+ * @param {string[]} keys
462
+ * @param {boolean} options.expandParent 是否展开其父节点
463
+ * @param {boolean} options.foldOthers 是否折叠其他父节点
464
+ */
465
+ expandItem(keys, options = {}) {
466
+ options = Object.assign({ expandParent: true, foldOthers: false }, options);
467
+
468
+ if (!keys?.length) {
469
+ if (Object.keys(this.currentItem).length) {
470
+ keys = [this.currentItem];
471
+ } else {
472
+ throw new Error('vScrollTree.expandItem Error: keys is empty');
473
+ }
474
+ }
475
+ // 需要展开的项的数量
476
+ let itemsLen = 0;
477
+ this.traverseTreeData(item => {
478
+ if (options.foldOthers) {
479
+ item._vt_isExpand = false;
480
+ }
481
+ if (keys.includes(item[this.assignedFields.key])) {
482
+ // 找到该项,设置为展开
483
+ item._vt_isExpand = true;
484
+ if (options.expandParent) {
485
+ // 展开其所有父节点
486
+ while (item._vt_parent) {
487
+ item = item._vt_parent;
488
+ item._vt_isExpand = true;
489
+ }
490
+ }
491
+ if (!options.foldOthers) {
492
+ itemsLen++;
493
+ if (itemsLen >= keys.length) {
494
+ // 已经找到所有,终止遍历
495
+ return 0;
496
+ }
497
+ }
498
+ }
499
+ });
500
+
501
+ // 更新展开数组
502
+ this.setTreeDataFlat();
503
+ },
504
+ /**
505
+ * 滚动到某一项,此项必须已经展开可见
506
+ * 默认滚动到defaultScrollKey
507
+ * @param {string} key
508
+ */
509
+ scrollTo(key = this.defaultScrollKey) {
510
+ this.$nextTick(() => {
511
+ this.$el.scrollTop = this.treeDataFlat.findIndex(it => it[this.assignedFields.key] === key) * this.lineHeight;
512
+ });
513
+ },
514
+ },
515
+ };
516
+ </script>
517
+
518
+ <style scoped lang="less">
519
+ .vtScroll-tree {
520
+ box-sizing: border-box;
521
+ background-color: #fff;
522
+ user-select: none;
523
+ width: 100%;
524
+ height: 100%;
525
+ // min-height: 300px;
526
+ display: flex;
527
+ flex-direction: column;
528
+ overflow: auto;
529
+ overflow: overlay;
530
+ &::-webkit-scrollbar {
531
+ width: 6px;
532
+ height: 6px;
533
+ }
534
+ &::-webkit-scrollbar-track {
535
+ border-radius: 5px;
536
+ }
537
+ &::-webkit-scrollbar-thumb {
538
+ background: rgba(74, 75, 114, 0.4);
539
+ border-radius: 5px;
540
+ &:hover {
541
+ background: rgba(74, 75, 114, 0.6);
542
+ }
543
+ }
544
+ &::-webkit-scrollbar-corner {
545
+ background: transparent;
546
+ }
547
+ ul {
548
+ height: 100%;
549
+ padding: 0;
550
+ margin: 0;
551
+ flex: 1;
552
+ // width: max-content;
553
+ width: 100%;
554
+ min-width: max-content;
555
+
556
+ li {
557
+ width: 100%;
558
+ min-width: max-content;
559
+ list-style-type: none;
560
+ .list-item {
561
+ box-sizing: border-box; //确保padding-right在min-width: 100%;里
562
+ color: #000;
563
+ min-width: 100%;
564
+ width: max-content;
565
+ display: flex;
566
+ padding-right: 10px;
567
+ cursor: pointer;
568
+ align-items: center;
569
+ &.item-current {
570
+ }
571
+ &.item-highlight {
572
+ color: #fff;
573
+ background-color: #1b63d9;
574
+ .list-item-expand .list-item-arrow {
575
+ border-left: 5px solid #fff;
576
+ }
577
+ }
578
+ &:hover:not(.item-highlight) {
579
+ background-color: #eee;
580
+ }
581
+
582
+ // 父节点
583
+ &.item-parent {
584
+ font-weight: bold;
585
+ }
586
+ .list-item-expand {
587
+ height: 16px;
588
+ width: 16px;
589
+ display: flex;
590
+ align-items: center;
591
+ justify-content: center;
592
+ &:hover {
593
+ opacity: 0.6;
594
+ }
595
+ /*箭头 */
596
+ .list-item-arrow {
597
+ transform-origin: 2px center;
598
+ border-left: 5px solid #757699; // color 继承自祖先元素
599
+ border-top: 4.5px solid transparent;
600
+ border-bottom: 4.5px solid transparent;
601
+ border-right: 0px;
602
+ transition: transform 0.2s ease;
603
+ &.list-item-arrow-active {
604
+ transform: rotate(90deg);
605
+ }
606
+ }
607
+ }
608
+ .list-item-title {
609
+ margin-left: 5px;
610
+ white-space: nowrap;
611
+ }
612
+ }
613
+ }
614
+ }
615
+ .vtScroll-empty {
616
+ height: 100%;
617
+ display: flex;
618
+ align-items: center;
619
+ justify-content: center;
620
+ }
621
+ }
622
+ </style>