stk-table-vue 0.0.1-beta.1

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,1042 @@
1
+ <template>
2
+ <div
3
+ ref="tableContainer"
4
+ class="stk-table"
5
+ :class="{
6
+ virtual,
7
+ 'virtual-x': virtualX,
8
+ dark: theme === 'dark',
9
+ headless,
10
+ 'is-col-resizing': isColResizing,
11
+ }"
12
+ :style="virtual && { '--row-height': virtualScroll.rowHeight + 'px' }"
13
+ @scroll="onTableScroll"
14
+ @wheel="onTableWheel"
15
+ >
16
+ <!-- 横向滚动时固定列的阴影,TODO: 覆盖一层在整个表上,使用linear-gradient 绘制阴影-->
17
+ <!-- <div
18
+ :class="showFixedLeftShadow && 'stk-table-fixed-left-col-box-shadow'"
19
+ :style="{ width: fixedLeftColWidth + 'px' }"
20
+ ></div> -->
21
+ <!-- 这个元素用于虚拟滚动时,撑开父容器的高度 (已弃用,因为滚动条拖动过快,下方tr为加载出来时,会导致表头sticky闪动)
22
+ <div
23
+ v-if="virtual"
24
+ class="virtual-table-height"
25
+ :style="{ height: dataSourceCopy.length * virtualScroll.rowHeight + 'px' }"
26
+ ></div>
27
+ -->
28
+ <div v-show="colResizable" ref="colResizeIndicator" class="column-resize-indicator"></div>
29
+ <!-- 表格主体 -->
30
+ <table class="stk-table-main" :style="{ width: tableWidth, minWidth, maxWidth }">
31
+ <!-- transform: virtualX_on ? `translateX(${virtualScrollX.offsetLeft}px)` : null, 用transform控制虚拟滚动左边距,sticky会有问题 -->
32
+ <thead v-if="!headless">
33
+ <tr v-for="(row, rowIndex) in tableHeaders" :key="rowIndex" @contextmenu="e => onHeaderMenu(e)">
34
+ <!-- 这个th用于横向虚拟滚动表格左边距,width、maxWidth 用于兼容低版本浏览器 -->
35
+ <th
36
+ v-if="virtualX_on"
37
+ class="virtual-x-left"
38
+ :style="{
39
+ minWidth: virtualScrollX.offsetLeft + 'px',
40
+ width: virtualScrollX.offsetLeft + 'px',
41
+ }"
42
+ ></th>
43
+ <!-- v for中最后一行才用 切割。TODO:不支持多级表头虚拟横向滚动 -->
44
+ <th
45
+ v-for="(col, colIndex) in virtualX_on && rowIndex === tableHeaders.length - 1 ? virtualX_columnPart : row"
46
+ :key="col.dataIndex"
47
+ :data-col-key="col.dataIndex"
48
+ :draggable="headerDrag ? 'true' : 'false'"
49
+ :rowspan="virtualX_on ? 1 : col.rowSpan"
50
+ :colspan="col.colSpan"
51
+ :style="getCellStyle(1, col)"
52
+ :title="col.title"
53
+ :class="[
54
+ col.sorter ? 'sortable' : '',
55
+ col.dataIndex === sortCol && sortOrderIndex !== 0 && 'sorter-' + sortSwitchOrder[sortOrderIndex],
56
+ showHeaderOverflow ? 'text-overflow' : '',
57
+ col.headerClassName,
58
+ col.fixed ? 'fixed-cell' : '',
59
+ ]"
60
+ @click="
61
+ e => {
62
+ onColumnSort(col);
63
+ onHeaderCellClick(e, col);
64
+ }
65
+ "
66
+ @dragstart="onThDragStart"
67
+ @drop="onThDrop"
68
+ @dragover="onThDragOver"
69
+ >
70
+ <div class="table-header-cell-wrapper">
71
+ <component :is="col.customHeaderCell" v-if="col.customHeaderCell" :col="col" />
72
+ <template v-else>
73
+ <slot name="tableHeader" :column="col">
74
+ <span class="table-header-title">{{ col.title }}</span>
75
+ </slot>
76
+ </template>
77
+
78
+ <!-- 排序图图标 -->
79
+ <span v-if="col.sorter" class="table-header-sorter">
80
+ <svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 16 16">
81
+ <g id="sort-btn">
82
+ <polygon id="arrow-up" fill="#757699" points="8 2 4.8 6 11.2 6"></polygon>
83
+ <polygon
84
+ id="arrow-down"
85
+ transform="translate(8, 12) rotate(-180) translate(-8, -12) "
86
+ points="8 10 4.8 14 11.2 14"
87
+ ></polygon>
88
+ </g>
89
+ </svg>
90
+ </span>
91
+ <!-- 列宽拖动handler -->
92
+ <div
93
+ v-if="colResizable && colIndex > 0"
94
+ class="table-header-resizer left"
95
+ @mousedown="e => onThResizeMouseDown(e, col, true)"
96
+ ></div>
97
+ <div
98
+ v-if="colResizable"
99
+ class="table-header-resizer right"
100
+ @mousedown="e => onThResizeMouseDown(e, col)"
101
+ ></div>
102
+ </div>
103
+ </th>
104
+ <!-- 这个th用于横向虚拟滚动表格右边距 width、maxWidth 用于兼容低版本浏览器-->
105
+ <th
106
+ v-if="virtualX_on"
107
+ style="padding: 0"
108
+ :style="{
109
+ minWidth: virtualX_offsetRight + 'px',
110
+ width: virtualX_offsetRight + 'px',
111
+ }"
112
+ ></th>
113
+ </tr>
114
+ </thead>
115
+
116
+ <!-- 用于虚拟滚动表格内容定位 @deprecated 有兼容问题-->
117
+ <!-- <tbody v-if="virtual_on" :style="{ height: `${virtualScroll.offsetTop}px` }">
118
+ <!==这个tr兼容火狐==>
119
+ <tr></tr>
120
+ </tbody> -->
121
+ <!-- <td
122
+ v-for="col in virtualX_on ? virtualX_columnPart : tableHeaderLast"
123
+ :key="col.dataIndex"
124
+ class="perch-td top"
125
+ ></td> -->
126
+ <!-- <tbody :style="{ transform: `translateY(${virtualScroll.offsetTop}px)` }"> -->
127
+ <tbody>
128
+ <tr v-if="virtual_on" :style="{ height: `${virtualScroll.offsetTop}px` }"></tr>
129
+ <tr
130
+ v-for="(row, i) in virtual_dataSourcePart"
131
+ :key="rowKey ? rowKeyGen(row) : i"
132
+ :data-row-key="rowKey ? rowKeyGen(row) : i"
133
+ :class="{
134
+ active: rowKey ? rowKeyGen(row) === (currentItem && rowKeyGen(currentItem)) : row === currentItem,
135
+ hover: rowKey ? rowKeyGen(row) === currentHover : row === currentHover,
136
+ [rowClassName(row, i)]: true,
137
+ }"
138
+ :style="{
139
+ backgroundColor: row._bgc,
140
+ }"
141
+ @click="e => onRowClick(e, row)"
142
+ @dblclick="e => onRowDblclick(e, row)"
143
+ @contextmenu="e => onRowMenu(e, row)"
144
+ @mouseover="e => onTrMouseOver(e, row)"
145
+ >
146
+ <!--这个td用于配合虚拟滚动的th对应,防止列错位-->
147
+ <td v-if="virtualX_on" class="virtual-x-left" style="padding: 0"></td>
148
+ <td
149
+ v-for="col in virtualX_columnPart"
150
+ :key="col.dataIndex"
151
+ :data-index="col.dataIndex"
152
+ :class="[col.className, showOverflow ? 'text-overflow' : '', col.fixed ? 'fixed-cell' : '']"
153
+ :style="getCellStyle(2, col)"
154
+ @click="e => onCellClick(e, row, col)"
155
+ >
156
+ <component :is="col.customCell" v-if="col.customCell" :col="col" :row="row" />
157
+ <div v-else class="table-cell-wrapper" :title="row[col.dataIndex]">
158
+ {{ row[col.dataIndex] ?? emptyCellText }}
159
+ </div>
160
+ </td>
161
+ </tr>
162
+ <tr v-if="virtual_on" :style="{ height: `${virtual_offsetBottom}px` }"></tr>
163
+ </tbody>
164
+ </table>
165
+ <div
166
+ v-if="(!dataSourceCopy || !dataSourceCopy.length) && showNoData"
167
+ class="stk-table-no-data"
168
+ :class="{ 'no-data-full': noDataFull }"
169
+ >
170
+ <slot name="empty">暂无数据</slot>
171
+ </div>
172
+ </div>
173
+ </template>
174
+
175
+ <script setup lang="ts">
176
+ /**
177
+ * @author JA+
178
+ * 不支持低版本浏览器非虚拟滚动表格的表头固定,列固定,因为会卡。
179
+ * TODO:存在的问题:
180
+ * [] column.dataIndex 作为唯一键,不能重复
181
+ * [] 计算的高亮颜色,挂在数据源上对象上,若多个表格使用同一个数据源对象会有问题。需要深拷贝。(解决方案:获取组件uid)
182
+ * [] highlight-row 颜色不能恢复到active的颜色
183
+ */
184
+ import { SortOption, StkProps, StkTableColumn } from '@/StkTable/types/index';
185
+ import { CSSProperties, computed, onMounted, ref, shallowRef, toRaw, watch } from 'vue';
186
+ import { Default_Col_Width, Highlight_Color_Change_Freq, Is_Legacy_Mode } from './const';
187
+ import { useColResize } from './useColResize';
188
+ import { useHighlight } from './useHighlight';
189
+ import { useThDrag } from './useThDrag';
190
+ import { useVirtualScroll } from './useVirtualScroll';
191
+ import { howDeepTheColumn, tableSort } from './utils';
192
+
193
+ const props = withDefaults(defineProps<StkProps>(), {
194
+ width: '',
195
+ minWidth: 'min-content',
196
+ maxWidth: '',
197
+ headless: false,
198
+ theme: 'light',
199
+ virtual: false,
200
+ virtualX: false,
201
+ columns: () => [],
202
+ dataSource: () => [],
203
+ rowKey: '',
204
+ colKey: 'dataIndex',
205
+ emptyCellText: '--',
206
+ noDataFull: false,
207
+ showNoData: true,
208
+ sortRemote: false,
209
+ showHeaderOverflow: false,
210
+ showOverflow: false,
211
+ showTrHoverClass: false,
212
+ headerDrag: false,
213
+ rowClassName: () => '',
214
+ colResizable: false,
215
+ colMinWidth: 10,
216
+ });
217
+
218
+ const emit = defineEmits([
219
+ 'sort-change',
220
+ 'row-click',
221
+ 'current-change',
222
+ 'row-dblclick',
223
+ 'header-row-menu',
224
+ 'row-menu',
225
+ 'cell-click',
226
+ 'header-cell-click',
227
+ 'scroll',
228
+ 'col-order-change',
229
+ 'th-drop',
230
+ 'th-drag-start',
231
+ 'columns',
232
+ ]);
233
+
234
+ const tableContainer = ref<HTMLDivElement>();
235
+ const colResizeIndicator = ref<HTMLDivElement>();
236
+ /** 当前选中的一行*/
237
+ const currentItem = ref(null);
238
+ /** 当前hover的行 */
239
+ const currentHover = ref(null);
240
+
241
+ /** 排序的列dataIndex*/
242
+ let sortCol = ref<string | null>();
243
+ let sortOrderIndex = ref(0);
244
+
245
+ /** 排序切换顺序 */
246
+ const sortSwitchOrder = [null, 'desc', 'asc'];
247
+
248
+ /** 表头.内容是 props.columns 的引用集合 */
249
+ const tableHeaders = ref<StkTableColumn<any>[][]>([]);
250
+ /** 若有多级表头时,最后一行的tableHeaders.内容是 props.columns 的引用集合 */
251
+ const tableHeaderLast = ref<StkTableColumn<any>[]>([]);
252
+
253
+ const dataSourceCopy = shallowRef([...props.dataSource]);
254
+
255
+ /**高亮帧间隔
256
+ const highlightStepDuration = Highlight_Color_Change_Freq / 1000 + 's';*/
257
+
258
+ /** rowKey缓存 */
259
+ const rowKeyGenStore = new WeakMap();
260
+
261
+ const tableWidth = computed(() => {
262
+ return props.colResizable ? 'fit-content' : props.width;
263
+ });
264
+
265
+ const fixedColumnsPositionStore = computed(() => {
266
+ const store: Record<string, number> = {};
267
+ const cols = [...tableHeaderLast.value];
268
+ let left = 0;
269
+ for (let i = 0; i < cols.length; i++) {
270
+ const item = cols[i];
271
+ if (item.fixed === 'left') {
272
+ store[item.dataIndex] = left;
273
+ left += parseInt(item.width || Default_Col_Width);
274
+ }
275
+ }
276
+ let right = 0;
277
+ for (let i = cols.length - 1; i >= 0; i--) {
278
+ const item = cols[i];
279
+ if (item.fixed === 'right') {
280
+ store[item.dataIndex] = right;
281
+ right += parseInt(item.width || Default_Col_Width);
282
+ }
283
+ }
284
+
285
+ return store;
286
+ });
287
+
288
+ const { isColResizing, onThResizeMouseDown } = useColResize({
289
+ props,
290
+ emit,
291
+ colKeyGen,
292
+ colResizeIndicator,
293
+ tableContainer,
294
+ tableHeaderLast,
295
+ });
296
+
297
+ const { onThDragStart, onThDragOver, onThDrop } = useThDrag({ emit });
298
+
299
+ const {
300
+ virtualScroll,
301
+ virtualScrollX,
302
+ virtual_on,
303
+ virtual_dataSourcePart,
304
+ virtual_offsetBottom,
305
+ virtualX_on,
306
+ virtualX_columnPart,
307
+ virtualX_offsetRight,
308
+ initVirtualScrollY,
309
+ initVirtualScrollX,
310
+ updateVirtualScrollY,
311
+ updateVirtualScrollX,
312
+ } = useVirtualScroll({ tableContainer, props, dataSourceCopy, tableHeaderLast });
313
+
314
+ /**
315
+ * 高亮行,高亮单元格
316
+ */
317
+ const { setHighlightDimCell, setHighlightDimRow } = useHighlight({ props, tableContainer, rowKeyGen });
318
+
319
+ watch(
320
+ () => props.columns,
321
+ () => {
322
+ dealColumns();
323
+ initVirtualScrollX();
324
+ },
325
+ );
326
+
327
+ dealColumns();
328
+
329
+ watch(
330
+ () => props.dataSource,
331
+ val => {
332
+ // dealColumns(val);
333
+ let needInitVirtualScrollY = false;
334
+ if (dataSourceCopy.value.length !== val.length) {
335
+ needInitVirtualScrollY = true;
336
+ }
337
+ dataSourceCopy.value = [...val];
338
+ // 数据长度没变则不计算虚拟滚动
339
+ if (needInitVirtualScrollY) initVirtualScrollY();
340
+
341
+ if (sortCol.value) {
342
+ // 排序
343
+ const column = tableHeaderLast.value.find(it => it.dataIndex === sortCol.value);
344
+ onColumnSort(column, false);
345
+ }
346
+ },
347
+ {
348
+ deep: false,
349
+ },
350
+ );
351
+ onMounted(() => {
352
+ initVirtualScroll();
353
+ });
354
+
355
+ /**
356
+ * 初始化虚拟滚动参数
357
+ * @param {number} [height] 虚拟滚动的高度
358
+ */
359
+ function initVirtualScroll(height?: number) {
360
+ initVirtualScrollY(height);
361
+ initVirtualScrollX();
362
+ }
363
+
364
+ /**
365
+ * 固定列的style
366
+ * @param {1|2} tagType 1-th 2-td
367
+ * @param {StkTableColumn} col
368
+ */
369
+ function getFixedStyle(tagType: 1 | 2, col: StkTableColumn<any>): CSSProperties {
370
+ const style: CSSProperties = {};
371
+ if (Is_Legacy_Mode) {
372
+ if (tagType === 1) {
373
+ style.position = 'relative';
374
+ style.top = virtualScroll.value.scrollTop + 'px';
375
+ }
376
+ }
377
+ const { fixed, dataIndex } = col;
378
+ if (fixed === 'left' || fixed === 'right') {
379
+ const isFixedLeft = fixed === 'left';
380
+ if (Is_Legacy_Mode) {
381
+ /**
382
+ * ----------浏览器兼容--------------
383
+ */
384
+ style.position = 'relative'; // 固定列方案替换为relative。原因:transform 在chrome84浏览器,列变动会导致横向滚动条计算出问题。
385
+ if (isFixedLeft) {
386
+ if (virtualX_on.value) style.left = virtualScrollX.value.scrollLeft - virtualScrollX.value.offsetLeft + 'px';
387
+ else style.left = virtualScrollX.value.scrollLeft + 'px';
388
+ } else {
389
+ // TODO:计算右侧距离
390
+ style.right = `${virtualX_offsetRight.value}px`;
391
+ }
392
+ if (tagType === 1) {
393
+ style.top = virtualScroll.value.scrollTop + 'px';
394
+ style.zIndex = isFixedLeft ? '4' : '3'; // 保证固定列高于其他单元格
395
+ } else {
396
+ style.zIndex = isFixedLeft ? '3' : '2';
397
+ }
398
+ } else {
399
+ /**
400
+ * -------------高版本浏览器----------------
401
+ */
402
+ style.position = 'sticky';
403
+ if (isFixedLeft) {
404
+ style.left = fixedColumnsPositionStore.value[dataIndex] + 'px';
405
+ } else {
406
+ style.right = fixedColumnsPositionStore.value[dataIndex] + 'px';
407
+ }
408
+ if (tagType === 1) {
409
+ style.top = '0';
410
+ style.zIndex = isFixedLeft ? '4' : '3'; // 保证固定列高于其他单元格
411
+ } else {
412
+ style.zIndex = isFixedLeft ? '3' : '2';
413
+ }
414
+ }
415
+ }
416
+
417
+ return style;
418
+ }
419
+
420
+ /**
421
+ * 处理多级表头
422
+ * FIXME: 仅支持到两级表头。不支持多级。
423
+ */
424
+ function dealColumns() {
425
+ // reset
426
+ tableHeaders.value = [];
427
+ tableHeaderLast.value = [];
428
+ const copyColumn = props.columns; // do not deep clone
429
+ const deep = howDeepTheColumn(copyColumn);
430
+ const tmpHeaderLast: StkTableColumn<any>[] = [];
431
+
432
+ if (deep > 1 && props.virtualX) {
433
+ console.error('多级表头不支持横向虚拟滚动');
434
+ }
435
+
436
+ // 展开columns
437
+ (function flat(arr: StkTableColumn<any>[], depth = 0) {
438
+ if (!tableHeaders.value[depth]) {
439
+ tableHeaders.value[depth] = [];
440
+ }
441
+ arr.forEach(col => {
442
+ col.rowSpan = col.children ? 1 : deep - depth;
443
+ col.colSpan = col.children?.length;
444
+ if (col.rowSpan === 1) delete col.rowSpan;
445
+ if (col.colSpan === 1) delete col.colSpan;
446
+ tableHeaders.value[depth].push(col);
447
+ if (!props.virtualX && col.children) {
448
+ flat(col.children, depth + 1);
449
+ } else {
450
+ tmpHeaderLast.push(col); // 没有children的列作为colgroup
451
+ }
452
+ });
453
+ })(copyColumn);
454
+
455
+ tableHeaderLast.value = tmpHeaderLast;
456
+ }
457
+
458
+ /**
459
+ * 行唯一值生成
460
+ */
461
+ function rowKeyGen(row: any) {
462
+ let key = rowKeyGenStore.get(row);
463
+ if (!key) {
464
+ key = typeof props.rowKey === 'function' ? props.rowKey(row) : row[props.rowKey];
465
+ rowKeyGenStore.set(row, key);
466
+ }
467
+ return key;
468
+ }
469
+
470
+ /**
471
+ * 列唯一键
472
+ * @param {StkTableColumn} col
473
+ */
474
+ function colKeyGen(col: StkTableColumn<any>) {
475
+ return typeof props.colKey === 'function' ? props.colKey(col) : (col as any)[props.colKey];
476
+ }
477
+
478
+ /**
479
+ * 性能优化,缓存style行内样式
480
+ *
481
+ * FIXME: col变化时仍从缓存拿style。watch col?
482
+ * @param {1|2} tagType 1-th 2-td
483
+ * @param {StkTableColumn} col
484
+ */
485
+ function getCellStyle(tagType: 1 | 2, col: StkTableColumn<any>): CSSProperties {
486
+ const fixedStyle = getFixedStyle(tagType, col);
487
+ const style: CSSProperties = {
488
+ width: col.width,
489
+ minWidth: props.colResizable ? col.width : col.minWidth || col.width,
490
+ maxWidth: props.colResizable ? col.width : col.maxWidth || col.width,
491
+ ...fixedStyle,
492
+ };
493
+ if (tagType === 1) {
494
+ // TH
495
+ style.textAlign = col.headerAlign;
496
+ } else if (tagType === 2) {
497
+ // TD
498
+ style.textAlign = col.align;
499
+ }
500
+
501
+ return style;
502
+ }
503
+
504
+ /**
505
+ * 表头点击排序
506
+ * @param {boolean} options.force sort-remote 开启后是否强制排序
507
+ * @param {boolean} options.emit 是否触发回调
508
+ */
509
+ function onColumnSort(col?: StkTableColumn<any>, click = true, options: { force?: boolean; emit?: boolean } = {}) {
510
+ if (!col?.sorter) return;
511
+ options = { force: false, emit: false, ...options };
512
+ if (sortCol.value !== col.dataIndex) {
513
+ // 改变排序的列时,重置排序
514
+ sortCol.value = col.dataIndex;
515
+ sortOrderIndex.value = 0;
516
+ }
517
+ if (click) sortOrderIndex.value++;
518
+ sortOrderIndex.value = sortOrderIndex.value % 3;
519
+
520
+ const order = sortSwitchOrder[sortOrderIndex.value];
521
+
522
+ if (!props.sortRemote || options.force) {
523
+ dataSourceCopy.value = tableSort(col, order, props.dataSource);
524
+ }
525
+ // 只有点击才触发事件
526
+ if (click || options.emit) {
527
+ emit('sort-change', col, order, toRaw(dataSourceCopy.value));
528
+ }
529
+ }
530
+
531
+ function onRowClick(e: MouseEvent, row: any) {
532
+ emit('row-click', e, row);
533
+ // 选中同一行不触发current-change 事件
534
+ if (currentItem.value === row) return;
535
+ currentItem.value = row;
536
+ emit('current-change', e, row);
537
+ }
538
+
539
+ function onRowDblclick(e: MouseEvent, row: any) {
540
+ emit('row-dblclick', e, row);
541
+ }
542
+
543
+ /** 表头行右键 */
544
+ function onHeaderMenu(e: MouseEvent) {
545
+ emit('header-row-menu', e);
546
+ }
547
+
548
+ /** 表体行右键 */
549
+ function onRowMenu(e: MouseEvent, row: any) {
550
+ emit('row-menu', e, row);
551
+ }
552
+
553
+ /** 单元格单击 */
554
+ function onCellClick(e: MouseEvent, row: any, col: StkTableColumn<any>) {
555
+ emit('cell-click', e, row, col);
556
+ }
557
+
558
+ /** 表头单元格单击 */
559
+ function onHeaderCellClick(e: MouseEvent, col: StkTableColumn<any>) {
560
+ emit('header-cell-click', e, col);
561
+ }
562
+
563
+ /**
564
+ * 鼠标滚轮事件监听
565
+ * @param {MouseEvent} e
566
+ */
567
+ function onTableWheel(e: MouseEvent) {
568
+ if (isColResizing.value) {
569
+ // 正在调整列宽时,不允许用户滚动
570
+ e.preventDefault();
571
+ e.stopPropagation();
572
+ return;
573
+ }
574
+ }
575
+
576
+ /**
577
+ * 滚动条监听
578
+ * @param {Event} e scrollEvent
579
+ */
580
+ function onTableScroll(e: Event) {
581
+ if (!e?.target) return;
582
+
583
+ // 此处可优化,因为访问e.target.scrollXX消耗性能
584
+ const { scrollTop, scrollLeft } = e.target as HTMLElement;
585
+ // 纵向滚动有变化
586
+ if (scrollTop !== virtualScroll.value.scrollTop) virtualScroll.value.scrollTop = scrollTop;
587
+ if (virtual_on.value) {
588
+ updateVirtualScrollY(scrollTop);
589
+ }
590
+
591
+ // 横向滚动有变化
592
+ if (scrollLeft !== virtualScrollX.value.scrollLeft) virtualScrollX.value.scrollLeft = scrollLeft;
593
+ if (virtualX_on.value) {
594
+ updateVirtualScrollX(scrollLeft);
595
+ }
596
+ emit('scroll', e);
597
+ }
598
+
599
+ /** tr hover事件 */
600
+ function onTrMouseOver(e: MouseEvent, row: any) {
601
+ if (props.showTrHoverClass) {
602
+ currentHover.value = rowKeyGen(row);
603
+ }
604
+ }
605
+
606
+ /**
607
+ * 选中一行,
608
+ * @param {string} rowKey
609
+ * @param {boolean} option.silent 是否触发回调
610
+ */
611
+ function setCurrentRow(rowKey: string, option = { silent: false }) {
612
+ if (!dataSourceCopy.value.length) return;
613
+ currentItem.value = dataSourceCopy.value.find(it => rowKeyGen(it) === rowKey);
614
+ if (!option.silent) {
615
+ emit('current-change', null, currentItem.value);
616
+ }
617
+ }
618
+
619
+ /**
620
+ * 设置表头排序状态
621
+ * @param {string} dataIndex 列字段
622
+ * @param {'asc'|'desc'|null} order
623
+ * @param {object} option.sortOption 指定排序参数
624
+ * @param {boolean} option.sort 是否触发排序
625
+ * @param {boolean} option.silent 是否触发回调
626
+ */
627
+ function setSorter(
628
+ dataIndex: string,
629
+ order: null | 'asc' | 'desc',
630
+ option: { sortOption?: SortOption; silent?: boolean; sort?: boolean } = {},
631
+ ) {
632
+ const newOption = { silent: true, sortOption: null, sort: true, ...option };
633
+ sortCol.value = dataIndex;
634
+ sortOrderIndex.value = sortSwitchOrder.findIndex(it => it === order);
635
+
636
+ if (newOption.sort && dataSourceCopy.value?.length) {
637
+ // 如果表格有数据,则进行排序
638
+ const column = newOption.sortOption || tableHeaderLast.value.find(it => it.dataIndex === sortCol.value);
639
+ if (column) onColumnSort(column, false, { force: true, emit: !newOption.silent });
640
+ else console.warn('Can not find column by dataIndex:', sortCol.value);
641
+ }
642
+ return dataSourceCopy.value;
643
+ }
644
+
645
+ /** 重置排序 */
646
+ function resetSorter() {
647
+ sortCol.value = null;
648
+ sortOrderIndex.value = 0;
649
+ dataSourceCopy.value = [...props.dataSource];
650
+ }
651
+
652
+ /** 滚动 */
653
+ function scrollTo(top = 0, left = 0) {
654
+ if (!tableContainer.value) return;
655
+ if (top !== null) tableContainer.value.scrollTop = top;
656
+ if (left !== null) tableContainer.value.scrollLeft = left;
657
+ }
658
+
659
+ /** 获取当前状态的表格数据 */
660
+ function getTableData() {
661
+ return toRaw(dataSourceCopy.value);
662
+ }
663
+
664
+ defineExpose({
665
+ setCurrentRow,
666
+ setHighlightDimCell,
667
+ setHighlightDimRow,
668
+ setSorter,
669
+ resetSorter,
670
+ scrollTo,
671
+ getTableData,
672
+ });
673
+ </script>
674
+
675
+ <style lang="less" scoped>
676
+ .stk-table {
677
+ // contain: strict;
678
+ --row-height: 28px;
679
+ --cell-padding-x: 8px;
680
+ --resize-handle-width: 4px;
681
+ --border-color: #e8eaec;
682
+ --border-width: 1px;
683
+ --td-bgc: #fff;
684
+ --th-bgc: #f8f8f9;
685
+ --tr-active-bgc: rgb(230, 247, 255);
686
+ --tr-hover-bgc: rgba(230, 247, 255, 0.7);
687
+ --bg-border-top: linear-gradient(180deg, var(--border-color) var(--border-width), transparent var(--border-width));
688
+ --bg-border-right: linear-gradient(270deg, var(--border-color) var(--border-width), transparent var(--border-width));
689
+ --bg-border-bottom: linear-gradient(0deg, var(--border-color) var(--border-width), transparent var(--border-width));
690
+ --bg-border-left: linear-gradient(90deg, var(--border-color) var(--border-width), transparent var(--border-width));
691
+ --highlight-color: #71a2fd;
692
+
693
+ --sort-arrow-color: #5d5f69;
694
+ --sort-arrow-hover-color: #8f90b5;
695
+ --sort-arrow-active-color: #1b63d9;
696
+ --sort-arrow-active-sub-color: #cbcbe1;
697
+ /** 列宽拖动指示器颜色 */
698
+ --col-resize-indicator-color: #cbcbe1;
699
+
700
+ position: relative;
701
+ overflow: auto;
702
+ display: flex;
703
+ flex-direction: column;
704
+
705
+ /**深色模式 */
706
+ &.dark {
707
+ --th-bgc: #181c21;
708
+ --td-bgc: #181c21;
709
+ --border-color: #26292e;
710
+ --tr-active-bgc: #283f63;
711
+ --tr-hover-bgc: #1a2b46;
712
+ --table-bgc: #181c21;
713
+ --highlight-color: #1e4c99; // 不能用rgba,因为固定列时,会变成半透明
714
+
715
+ --sort-arrow-color: #5d6064;
716
+ --sort-arrow-hover-color: #727782;
717
+ --sort-arrow-active-color: #d0d1d2;
718
+ --sort-arrow-active-sub-color: #5d6064;
719
+
720
+ --col-resize-indicator-color: #5d6064;
721
+
722
+ // background-color: var(--table-bgc); // ⭐这里加background-color会导致表格出滚动白屏
723
+ color: #d0d1d2;
724
+ }
725
+
726
+ // .stk-table-fixed-left-col-box-shadow {
727
+ // position: sticky;
728
+ // left: 0;
729
+ // top: 0;
730
+ // height: 100%;
731
+ // box-shadow: 0 0 10px;
732
+ // z-index: 1;
733
+ // pointer-events: none;
734
+ // }
735
+ &.headless {
736
+ border-top: 1px solid var(--border-color);
737
+ }
738
+
739
+ /** 调整列宽的时候不要触发th事件。否则会导致触发表头点击排序 */
740
+ &.is-col-resizing th {
741
+ pointer-events: none;
742
+ }
743
+
744
+ /** 列宽调整指示器 */
745
+ .column-resize-indicator {
746
+ width: 0;
747
+ height: 100%;
748
+ border-left: 1px dashed var(--col-resize-indicator-color);
749
+ position: absolute;
750
+ z-index: 10;
751
+ display: none;
752
+ pointer-events: none;
753
+ }
754
+
755
+ .stk-table-main {
756
+ border-spacing: 0;
757
+ border-collapse: separate;
758
+
759
+ th,
760
+ td {
761
+ z-index: 1;
762
+ height: var(--row-height);
763
+ font-size: 14px;
764
+ box-sizing: border-box;
765
+ padding: 0 var(--cell-padding-x);
766
+ background-image: var(--bg-border-right), var(--bg-border-bottom);
767
+ }
768
+
769
+ thead {
770
+ tr {
771
+ &:first-child th {
772
+ position: sticky;
773
+ top: 0;
774
+ // border-top: 1px solid var(--border-color);
775
+ background-image: var(--bg-border-top), var(--bg-border-right), var(--bg-border-bottom);
776
+
777
+ &:first-child {
778
+ background-image: var(--bg-border-top), var(--bg-border-right), var(--bg-border-bottom),
779
+ var(--bg-border-left);
780
+ }
781
+ }
782
+
783
+ th {
784
+ background-color: var(--th-bgc);
785
+
786
+ &.sortable {
787
+ cursor: pointer;
788
+ }
789
+
790
+ &:first-child {
791
+ // border-left: 1px solid var(--border-color);
792
+ background-image: var(--bg-border-right), var(--bg-border-bottom), var(--bg-border-left);
793
+ // padding-left: 12px;
794
+ }
795
+
796
+ // &:last-child {
797
+ // padding-right: 12px;
798
+ // }
799
+ &.text-overflow {
800
+ .table-header-cell-wrapper {
801
+ white-space: nowrap;
802
+ overflow: hidden;
803
+
804
+ .table-header-title {
805
+ text-overflow: ellipsis;
806
+ overflow: hidden;
807
+ }
808
+ }
809
+ }
810
+
811
+ &:not(.sorter-desc):not(.sorter-asc):hover .table-header-cell-wrapper .table-header-sorter {
812
+ #arrow-up {
813
+ fill: var(--sort-arrow-hover-color);
814
+ }
815
+
816
+ #arrow-down {
817
+ fill: var(--sort-arrow-hover-color);
818
+ }
819
+ }
820
+
821
+ &.sorter-desc .table-header-cell-wrapper .table-header-sorter {
822
+ // display:initial;
823
+ #arrow-up {
824
+ fill: var(--sort-arrow-active-sub-color);
825
+ }
826
+
827
+ #arrow-down {
828
+ fill: var(--sort-arrow-active-color);
829
+ }
830
+ }
831
+
832
+ &.sorter-asc .table-header-cell-wrapper .table-header-sorter {
833
+ // display:initial;
834
+ #arrow-up {
835
+ fill: var(--sort-arrow-active-color);
836
+ }
837
+
838
+ #arrow-down {
839
+ fill: var(--sort-arrow-active-sub-color);
840
+ }
841
+ }
842
+
843
+ .table-header-cell-wrapper {
844
+ max-width: 100%; //最大宽度不超过列宽
845
+ display: inline-flex;
846
+ align-items: center;
847
+
848
+ .table-header-title {
849
+ overflow: hidden;
850
+ align-self: flex-start;
851
+ }
852
+
853
+ .table-header-sorter {
854
+ flex-shrink: 0;
855
+ margin-left: 4px;
856
+ width: 16px;
857
+ height: 16px;
858
+
859
+ // display:none;
860
+ #arrow-up,
861
+ #arrow-down {
862
+ fill: var(--sort-arrow-color);
863
+ }
864
+ }
865
+
866
+ .table-header-resizer {
867
+ position: absolute;
868
+ top: 0;
869
+ bottom: 0;
870
+ cursor: col-resize;
871
+ width: var(--resize-handle-width);
872
+
873
+ &.left {
874
+ left: 0;
875
+ }
876
+
877
+ &.right {
878
+ right: 0;
879
+ }
880
+ }
881
+ }
882
+ }
883
+ }
884
+ }
885
+
886
+ tbody {
887
+ /**高亮渐暗 */
888
+ @keyframes dim {
889
+ from {
890
+ background-color: var(--highlight-color);
891
+ }
892
+ }
893
+
894
+ tr {
895
+ background-color: var(--td-bgc); // td inherit tr bgc
896
+
897
+ &.highlight-row {
898
+ animation: dim 2s linear;
899
+ }
900
+
901
+ // &.highlight-row-transition {
902
+ // transition: background-color v-bind(highlightStepDuration) linear;
903
+ // }
904
+
905
+ &.hover,
906
+ &:hover {
907
+ background-color: var(--tr-hover-bgc);
908
+ }
909
+
910
+ &.active {
911
+ background-color: var(--tr-active-bgc);
912
+ }
913
+
914
+ td {
915
+ &.fixed-cell {
916
+ background-color: inherit; // 防止横向滚动后透明
917
+ }
918
+
919
+ &:first-child {
920
+ background-image: var(--bg-border-right), var(--bg-border-bottom), var(--bg-border-left);
921
+ }
922
+
923
+ &.highlight-cell {
924
+ animation: dim 2s linear;
925
+ }
926
+
927
+ &.text-overflow {
928
+ .table-cell-wrapper {
929
+ white-space: nowrap;
930
+ overflow: hidden;
931
+ text-overflow: ellipsis;
932
+ }
933
+ }
934
+
935
+ // &.perch-td {
936
+ // padding: 0;
937
+ // height: 0;
938
+ // &.top {
939
+ // background-image: repeating-linear-gradient(
940
+ // 180deg,
941
+ // transparent 0,
942
+ // transparent var(--row-height),
943
+ // var(--border-color) var(--row-height),
944
+ // var(--border-color) calc(var(--row-height) + 1px)
945
+ // ),
946
+ // var(--bg-border-right);
947
+ // }
948
+ // &.bottom {
949
+ // background-image: repeating-linear-gradient(
950
+ // 0deg,
951
+ // transparent 0,
952
+ // transparent var(--row-height),
953
+ // var(--border-color) var(--row-height),
954
+ // var(--border-color) calc(var(--row-height) + 1px)
955
+ // ),
956
+ // var(--bg-border-right);
957
+ // }
958
+ // }
959
+ }
960
+ }
961
+
962
+ // 斑马纹
963
+ // tr:nth-child(2n) td {
964
+ // background-color: #fafafc;
965
+ // }
966
+ // tr:hover {
967
+ // background-color: #ebf3ff;
968
+ // }
969
+ }
970
+ }
971
+
972
+ .stk-table-no-data {
973
+ background-color: var(--table-bgc);
974
+ line-height: var(--row-height);
975
+ text-align: center;
976
+ font-size: 14px;
977
+ position: sticky;
978
+ left: 0px;
979
+ border-left: var(--border-width) solid var(--border-color);
980
+ border-right: var(--border-width) solid var(--border-color);
981
+ border-bottom: var(--border-width) solid var(--border-color);
982
+ display: flex;
983
+ flex-direction: column;
984
+ align-items: center;
985
+ justify-content: center;
986
+
987
+ &.no-data-full {
988
+ flex: 1;
989
+ }
990
+ }
991
+
992
+ /**虚拟滚动模式 */
993
+ &.virtual {
994
+ .stk-table-main {
995
+ thead {
996
+ tr {
997
+ th {
998
+ // 为不影响布局,表头行高要定死
999
+ .table-header-cell-wrapper {
1000
+ overflow: hidden;
1001
+ max-height: var(--row-height);
1002
+ }
1003
+ }
1004
+ }
1005
+ }
1006
+
1007
+ tbody {
1008
+ position: relative;
1009
+
1010
+ tr {
1011
+ td {
1012
+ height: var(--row-height);
1013
+ line-height: 1;
1014
+
1015
+ .table-cell-wrapper {
1016
+ max-height: var(--row-height);
1017
+ overflow: hidden;
1018
+ }
1019
+ }
1020
+ }
1021
+ }
1022
+ }
1023
+ }
1024
+
1025
+ &.virtual-x {
1026
+ .stk-table-main {
1027
+ .virtual-x-left {
1028
+ padding: 0;
1029
+ }
1030
+
1031
+ thead tr:first-child .virtual-x-left + th {
1032
+ // 横向虚拟滚动时,左侧第一个单元格加上border-left
1033
+ background-image: var(--bg-border-top), var(--bg-border-right), var(--bg-border-bottom), var(--bg-border-left);
1034
+ }
1035
+
1036
+ tr .virtual-x-left + th {
1037
+ background-image: var(--bg-border-right), var(--bg-border-bottom), var(--bg-border-left);
1038
+ }
1039
+ }
1040
+ }
1041
+ }
1042
+ </style>