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,1686 @@
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': colResizeState.isResizing,
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="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
72
+ :is="typeof col.customHeaderCell === 'function' ? col.customHeaderCell(col) : col.customHeaderCell"
73
+ v-if="col.customHeaderCell"
74
+ :col="col"
75
+ />
76
+ <template v-else>
77
+ <slot name="tableHeader" :column="col">
78
+ <span class="table-header-title">{{ col.title }}</span>
79
+ </slot>
80
+ </template>
81
+
82
+ <!-- 排序图图标 -->
83
+ <span v-if="col.sorter" class="table-header-sorter">
84
+ <svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 16 16">
85
+ <g id="sort-btn">
86
+ <polygon id="arrow-up" fill="#757699" points="8 2 4.8 6 11.2 6"></polygon>
87
+ <polygon
88
+ id="arrow-down"
89
+ transform="translate(8, 12) rotate(-180) translate(-8, -12) "
90
+ points="8 10 4.8 14 11.2 14"
91
+ ></polygon>
92
+ </g>
93
+ </svg>
94
+ </span>
95
+ <!-- 列宽拖动handler -->
96
+ <div
97
+ v-if="colResizable && colIndex > 0"
98
+ class="table-header-resizer left"
99
+ @mousedown="e => onThResizeMouseDown(e, col, true)"
100
+ ></div>
101
+ <div
102
+ v-if="colResizable"
103
+ class="table-header-resizer right"
104
+ @mousedown="e => onThResizeMouseDown(e, col)"
105
+ ></div>
106
+ </div>
107
+ </th>
108
+ <!-- 这个th用于横向虚拟滚动表格右边距 width、maxWidth 用于兼容低版本浏览器-->
109
+ <th
110
+ v-if="virtualX_on"
111
+ style="padding: 0"
112
+ :style="{
113
+ minWidth: virtualX_offsetRight + 'px',
114
+ width: virtualX_offsetRight + 'px',
115
+ }"
116
+ ></th>
117
+ </tr>
118
+ </thead>
119
+
120
+ <!-- 用于虚拟滚动表格内容定位 @deprecated 有兼容问题-->
121
+ <!-- <tbody v-if="virtual_on" :style="{ height: `${virtualScroll.offsetTop}px` }">
122
+ <!==这个tr兼容火狐==>
123
+ <tr></tr>
124
+ </tbody> -->
125
+ <!-- <td
126
+ v-for="col in virtualX_on ? virtualX_columnPart : tableHeaderLast"
127
+ :key="col.dataIndex"
128
+ class="perch-td top"
129
+ ></td> -->
130
+ <!-- <tbody :style="{ transform: `translateY(${virtualScroll.offsetTop}px)` }"> -->
131
+ <tbody>
132
+ <tr v-if="virtual_on" :style="{ height: `${virtualScroll.offsetTop}px` }"></tr>
133
+ <tr
134
+ v-for="(row, i) in virtual_dataSourcePart"
135
+ :key="rowKey ? rowKeyGen(row) : i"
136
+ :data-row-key="rowKey ? rowKeyGen(row) : i"
137
+ :class="{
138
+ active: rowKey
139
+ ? rowKeyGen(row) === (currentItem.value && rowKeyGen(currentItem.value))
140
+ : row === currentItem.value,
141
+ hover: rowKey ? rowKeyGen(row) === currentHover.value : row === currentHover.value,
142
+ [rowClassName(row, i)]: true,
143
+ }"
144
+ :style="{
145
+ backgroundColor: row._bgc,
146
+ }"
147
+ @click="e => onRowClick(e, row)"
148
+ @dblclick="e => onRowDblclick(e, row)"
149
+ @contextmenu="e => onRowMenu(e, row)"
150
+ @mouseover="e => onTrMouseOver(e, row)"
151
+ >
152
+ <!--这个td用于配合虚拟滚动的th对应,防止列错位-->
153
+ <td v-if="virtualX_on" class="virtual-x-left" style="padding: 0"></td>
154
+ <td
155
+ v-for="col in virtualX_columnPart"
156
+ :key="col.dataIndex"
157
+ :data-index="col.dataIndex"
158
+ :class="[col.className, showOverflow ? 'text-overflow' : '', col.fixed ? 'fixed-cell' : '']"
159
+ :style="getCellStyle(2, col)"
160
+ @click="e => onCellClick(e, row, col)"
161
+ >
162
+ <component :is="col.customCell" v-if="col.customCell" :col="col" :row="row" />
163
+ <div v-else class="table-cell-wrapper" :title="row[col.dataIndex]">
164
+ {{ row[col.dataIndex] ?? emptyCellText }}
165
+ </div>
166
+ </td>
167
+ </tr>
168
+ <tr v-if="virtual_on" :style="{ height: `${virtual_offsetBottom}px` }"></tr>
169
+ </tbody>
170
+ </table>
171
+ <div
172
+ v-if="(!dataSourceCopy || !dataSourceCopy.length) && showNoData"
173
+ class="stk-table-no-data"
174
+ :class="{ 'no-data-full': noDataFull }"
175
+ >
176
+ <slot name="empty">暂无数据</slot>
177
+ </div>
178
+ </div>
179
+ </template>
180
+
181
+ <script>
182
+ /**
183
+ * @version 1.3.0
184
+ * @author JA+
185
+ * 不支持低版本浏览器非虚拟滚动表格的表头固定,列固定,因为会卡。
186
+ * TODO:存在的问题:
187
+ * [] column.dataIndex 作为唯一键,不能重复
188
+ * [] 计算的高亮颜色,挂在数据源上对象上,若多个表格使用同一个数据源对象会有问题。需要深拷贝。(解决方案:获取组件uid)
189
+ * [] highlight-row 颜色不能恢复到active的颜色
190
+ */
191
+ import { interpolateRgb } from 'd3-interpolate';
192
+ import { defineComponent } from 'vue';
193
+
194
+ /**
195
+ * @typedef {import('./StkTable').StkTableColumn<any>} StkTableColumn
196
+ */
197
+
198
+ let _chromeVersion = 0;
199
+ try {
200
+ _chromeVersion = +navigator.userAgent.match(/chrome\/\d+/i)[0].split('/')[1];
201
+ } catch (e) {
202
+ console.error('获取浏览器版本出错!', e);
203
+ }
204
+ /** 是否兼容低版本模式 */
205
+ const _isLegacyMode = _chromeVersion < 56;
206
+
207
+ /** 高亮背景色 */
208
+ const _highlightColor = {
209
+ light: { from: '#71a2fd', to: '#fff' },
210
+ dark: { from: '#1e4c99', to: '#181c21' },
211
+ };
212
+ /** 高亮持续时间 */
213
+ const _highlightDuration = 2000;
214
+ /** 高亮变更频率 */
215
+ const _highlightColorChangeFreq = 100;
216
+
217
+ function _howDeepTheColumn(arr, level = 1) {
218
+ const levels = [level];
219
+ arr.forEach(item => {
220
+ if (item.children?.length) {
221
+ levels.push(_howDeepTheColumn(item.children, level + 1));
222
+ }
223
+ });
224
+ return Math.max(...levels);
225
+ }
226
+ /**
227
+ * 对有序数组插入新数据
228
+ * @param {object} sortState
229
+ * @param {string} sortState.dataIndex 排序的列
230
+ * @param {null|'asc'|'desc'} sortState.order 排序顺序
231
+ * @param {'number'|'string'} [sortState.sortType] 排序方式
232
+ * @param {object} newItem 要插入的数据
233
+ * @param {Array} targetArray 表格数据
234
+ */
235
+ export function insertToOrderedArray(sortState, newItem, targetArray) {
236
+ let { dataIndex, order, sortType } = sortState;
237
+ if (!sortType) sortType = typeof newItem[dataIndex];
238
+ const data = [...targetArray];
239
+ if (!order) {
240
+ data.unshift(newItem);
241
+ return data;
242
+ }
243
+ // 二分插入
244
+ let sIndex = 0;
245
+ let eIndex = data.length - 1;
246
+ const targetVal = newItem[dataIndex];
247
+ while (sIndex <= eIndex) {
248
+ // console.log(sIndex, eIndex);
249
+ const midIndex = Math.floor((sIndex + eIndex) / 2);
250
+ const midVal = data[midIndex][dataIndex];
251
+ const compareRes = strCompare(midVal, targetVal, sortType);
252
+ if (compareRes === 0) {
253
+ //midVal == targetVal
254
+ sIndex = midIndex;
255
+ break;
256
+ } else if (compareRes === -1) {
257
+ // midVal < targetVal
258
+ if (order === 'asc') sIndex = midIndex + 1;
259
+ else eIndex = midIndex - 1;
260
+ } else {
261
+ //midVal > targetVal
262
+ if (order === 'asc') eIndex = midIndex - 1;
263
+ else sIndex = midIndex + 1;
264
+ }
265
+ }
266
+ data.splice(sIndex, 0, newItem);
267
+ return data;
268
+ }
269
+ /**
270
+ * 字符串比较
271
+ * @param {string} a
272
+ * @param {string} b
273
+ * @param {'number'|'string'} [type] 类型
274
+ * @return {-1|0|1}
275
+ */
276
+ function strCompare(a, b, type) {
277
+ // if (typeof a === 'number' && typeof b === 'number') type = 'number';
278
+ if (type === 'number') {
279
+ if (+a > +b) return 1;
280
+ if (+a === +b) return 0;
281
+ if (+a < +b) return -1;
282
+ } else {
283
+ return String(a).localeCompare(b);
284
+ }
285
+ }
286
+
287
+ /**
288
+ * @typedef SortOption
289
+ * @prop {function|boolean} sorter
290
+ * @prop {string} dataIndex
291
+ * @prop {string} sortField
292
+ * @prop {'number'|'string'} sortType
293
+ */
294
+ /**
295
+ * 表格排序抽离
296
+ * 可以在组件外部自己实现表格排序,组件配置remote,使表格不排序。
297
+ * 使用者在@sort-change事件中自行更改table props 'dataSource'完成排序。
298
+ * TODO: key 唯一值,排序字段相同时,根据唯一值排序。
299
+ * @param {SortOption} sortOption 列配置
300
+ * @param {string|null} order 排序方式
301
+ * @param {any} dataSource 排序的数组
302
+ */
303
+ export function tableSort(sortOption, order, dataSource) {
304
+ let targetDataSource = [...dataSource];
305
+ if (typeof sortOption.sorter === 'function') {
306
+ const customSorterData = sortOption.sorter(targetDataSource, { order, column: sortOption });
307
+ if (customSorterData) targetDataSource = customSorterData;
308
+ } else if (order) {
309
+ const sortField = sortOption.sortField || sortOption.dataIndex;
310
+ let { sortType } = sortOption;
311
+ if (!sortType) sortType = typeof dataSource[0][sortField];
312
+
313
+ if (sortType === 'number') {
314
+ // 按数字类型排序
315
+ const nanArr = []; // 非数字
316
+ const numArr = []; // 数字
317
+
318
+ for (let i = 0; i < targetDataSource.length; i++) {
319
+ const row = targetDataSource[i];
320
+ if (
321
+ row[sortField] === null ||
322
+ row[sortField] === '' ||
323
+ typeof row[sortField] === 'boolean' ||
324
+ Number.isNaN(+row[sortField])
325
+ ) {
326
+ nanArr.push(row);
327
+ } else {
328
+ numArr.push(row);
329
+ }
330
+ }
331
+ // 非数字当作最小值处理
332
+ if (order === 'asc') {
333
+ numArr.sort((a, b) => +a[sortField] - +b[sortField]);
334
+ targetDataSource = [...nanArr, ...numArr];
335
+ } else {
336
+ numArr.sort((a, b) => +b[sortField] - +a[sortField]);
337
+ targetDataSource = [...numArr, ...nanArr];
338
+ }
339
+ // targetDataSource = [...numArr, ...nanArr]; // 非数字不进入排序,一直排在最后
340
+ } else {
341
+ // 按string 排序
342
+ if (order === 'asc') {
343
+ targetDataSource.sort((a, b) => String(a[sortField]).localeCompare(b[sortField]));
344
+ } else {
345
+ targetDataSource.sort((a, b) => String(a[sortField]).localeCompare(b[sortField]) * -1);
346
+ }
347
+ }
348
+ }
349
+ return targetDataSource;
350
+ }
351
+
352
+ export default defineComponent({
353
+ name: 'StkTable',
354
+ props: {
355
+ width: {
356
+ type: String,
357
+ default: '',
358
+ },
359
+ /** 最小表格宽度 */
360
+ minWidth: {
361
+ type: String,
362
+ default: 'min-content',
363
+ },
364
+ /** 表格最大宽度*/
365
+ maxWidth: {
366
+ type: String,
367
+ default: '',
368
+ },
369
+ /** 是否隐藏表头 */
370
+ headless: {
371
+ type: Boolean,
372
+ default: false,
373
+ },
374
+ /** 主题,亮、暗 */
375
+ theme: {
376
+ type: String,
377
+ default: 'light',
378
+ validator: v => ['dark', 'light'].includes(v),
379
+ },
380
+ /** 虚拟滚动 */
381
+ virtual: {
382
+ type: Boolean,
383
+ default: false,
384
+ },
385
+ /** x轴虚拟滚动 */
386
+ virtualX: {
387
+ type: Boolean,
388
+ default: false,
389
+ },
390
+ /** 表格列配置 */
391
+ columns: {
392
+ type: Array,
393
+ default: () => [],
394
+ },
395
+ /** 表格数据源 */
396
+ dataSource: {
397
+ type: Array,
398
+ default: () => [],
399
+ },
400
+ /** 行唯一键 */
401
+ rowKey: {
402
+ type: [String, Function],
403
+ default: '',
404
+ },
405
+ /** 列唯一键 */
406
+ colKey: {
407
+ type: [String, Function],
408
+ default: 'dataIndex',
409
+ },
410
+ /** 空值展示文字 */
411
+ emptyCellText: {
412
+ type: String,
413
+ default: '--',
414
+ },
415
+ /** 暂无数据兜底高度是否撑满 */
416
+ noDataFull: {
417
+ type: Boolean,
418
+ default: false,
419
+ },
420
+ /** 是否展示暂无数据 */
421
+ showNoData: {
422
+ type: Boolean,
423
+ default: true,
424
+ },
425
+ /** 是否服务端排序,true则不排序数据 */
426
+ sortRemote: {
427
+ type: Boolean,
428
+ default: false,
429
+ },
430
+ /** 表头是否溢出展示... */
431
+ showHeaderOverflow: {
432
+ type: Boolean,
433
+ default: false,
434
+ },
435
+ /** 表体溢出是否展示... */
436
+ showOverflow: {
437
+ type: Boolean,
438
+ default: false,
439
+ },
440
+ /** 是否增加行hover class */
441
+ showTrHoverClass: {
442
+ type: Boolean,
443
+ default: false,
444
+ },
445
+ /** 表头是否可拖动 */
446
+ headerDrag: {
447
+ type: Boolean,
448
+ default: false,
449
+ },
450
+ /**
451
+ * 给行附加className<br>
452
+ * FIXME: 是否需要优化,因为不传此prop会使表格行一直执行空函数,是否有影响
453
+ */
454
+ rowClassName: {
455
+ type: Function,
456
+ default: () => '',
457
+ },
458
+ /**
459
+ * 列宽是否可拖动<br>
460
+ * **不要设置**列minWidth,**必须**设置width<br>
461
+ * 列宽拖动时,每一列都必须要有width,且minWidth/maxWidth不生效。table width会变为"fit-content"。
462
+ */
463
+ colResizable: {
464
+ type: Boolean,
465
+ default: false,
466
+ },
467
+ /** 可拖动至最小的列宽 */
468
+ colMinWidth: {
469
+ type: Number,
470
+ default: 10,
471
+ },
472
+ },
473
+ emits: [
474
+ 'row-click',
475
+ 'sort-change',
476
+ 'current-change',
477
+ 'row-dblclick',
478
+ 'header-row-menu',
479
+ 'row-menu',
480
+ 'cell-click',
481
+ 'header-cell-click',
482
+ 'col-order-change',
483
+ 'th-drop',
484
+ 'th-drag-start',
485
+ 'scroll',
486
+ 'update:columns',
487
+ ],
488
+ data() {
489
+ return {
490
+ /** 是否展示横向滚动固定列的阴影
491
+ showFixedLeftShadow: false,*/
492
+
493
+ /** 当前选中的一行*/
494
+ currentItem: { value: null },
495
+ /** 当前hover的行 */
496
+ currentHover: { value: null },
497
+ /** 排序的列*/
498
+ sortCol: null,
499
+ sortOrderIndex: 0,
500
+ /** 排序切换顺序 */
501
+ sortSwitchOrder: [null, 'desc', 'asc'],
502
+ /** 表头.内容是 props.columns 的引用集合 */
503
+ tableHeaders: [],
504
+ /** 若有多级表头时,的tableHeaders.内容是 props.columns 的引用集合 */
505
+ tableHeaderLast: [],
506
+ dataSourceCopy: Object.freeze([]),
507
+ /** 存放高亮行的对象*/
508
+ highlightDimRows: new Set(),
509
+ /** 高亮后渐暗的行定时器 */
510
+ highlightDimRowsTimeout: new Map(),
511
+ /** 高亮后渐暗的单元格定时器 */
512
+ highlightDimCellsTimeout: new Map(),
513
+ /** 是否正在计算高亮行的循环*/
514
+ calcHighlightDimLoop: false,
515
+ virtualScroll: {
516
+ containerHeight: 0,
517
+ startIndex: 0, // 数组开始位置
518
+ rowHeight: 28,
519
+ offsetTop: 0, // 表格定位上边距
520
+ scrollTop: 0, // 纵向滚动条位置,用于判断是横向滚动还是纵向
521
+ },
522
+ virtualScrollX: {
523
+ containerWidth: 0,
524
+ startIndex: 0,
525
+ endIndex: 0,
526
+ offsetLeft: 0,
527
+ scrollLeft: 0, // 横向滚动位置,用于判断是横向滚动还是纵向
528
+ },
529
+ thDrag: {
530
+ dragStartKey: null,
531
+ },
532
+ /** rowKey缓存 */
533
+ rowKeyGenStore: new WeakMap(),
534
+
535
+ /** 列宽调整状态 */
536
+ colResizeState: {
537
+ isResizing: false,
538
+ /** 初始宽度
539
+ originColWidth: 0,*/
540
+ /** 当前被拖动的列 @type {StkTableColumn}*/
541
+ currentCol: null,
542
+ /** 当前被拖动列的下标 */
543
+ currentColIndex: 0,
544
+ /** 鼠标按下开始位置 */
545
+ startX: 0,
546
+ /** 鼠标按下时鼠标对于表格的偏移量 */
547
+ startOffsetTableX: 0,
548
+ },
549
+ };
550
+ },
551
+ computed: {
552
+ tableWidth() {
553
+ return this.colResizable ? 'fit-content' : this.width;
554
+ },
555
+ /** 高亮颜色插值方法 */
556
+ highlightInter() {
557
+ return interpolateRgb(_highlightColor[this.theme].from, _highlightColor[this.theme].to);
558
+ },
559
+
560
+ /** 数据量大于2页才开始虚拟滚动*/
561
+ virtual_on() {
562
+ return this.virtual && this.dataSourceCopy.length > this.virtual_pageSize * 2;
563
+ },
564
+ /** 虚拟滚动展示的行数 */
565
+ virtual_pageSize() {
566
+ // 这里最终+1,因为headless=true无头时,需要上下各预渲染一行。
567
+ return Math.ceil(this.virtualScroll.containerHeight / this.virtualScroll.rowHeight) + 1;
568
+ },
569
+ /** 虚拟滚动展示的行 */
570
+ virtual_dataSourcePart() {
571
+ if (!this.virtual_on) return this.dataSourceCopy;
572
+ return this.dataSourceCopy.slice(
573
+ this.virtualScroll.startIndex,
574
+ this.virtualScroll.startIndex + this.virtual_pageSize,
575
+ );
576
+ },
577
+ /** 虚拟表格定位下边距*/
578
+ virtual_offsetBottom() {
579
+ if (!this.virtual_on) return 0;
580
+ return (
581
+ (this.dataSourceCopy.length - this.virtualScroll.startIndex - this.virtual_dataSourcePart.length) *
582
+ this.virtualScroll.rowHeight
583
+ );
584
+ },
585
+ /* 是否开启横向虚拟滚动 */
586
+ virtualX_on() {
587
+ return (
588
+ this.virtualX &&
589
+ this.tableHeaderLast.reduce((sum, col) => (sum += parseInt(col.minWidth || col.width)), 0) >
590
+ this.virtualScrollX.containerWidth * 1.5
591
+ );
592
+ },
593
+ /** 横向虚拟滚动展示的列,内容是 props.columns 的引用集合 */
594
+ virtualX_columnPart() {
595
+ if (this.virtualX_on) {
596
+ // 虚拟横向滚动,固定列要一直保持存在
597
+ const leftCols = [];
598
+ const rightCols = [];
599
+ // 左侧固定列,如果在左边不可见区。则需要拿出来放在前面
600
+ for (let i = 0; i < this.virtualScrollX.startIndex; i++) {
601
+ const col = this.tableHeaderLast[i];
602
+ if (col.fixed === 'left') leftCols.push(col);
603
+ }
604
+ // 右侧固定列,如果在右边不可见区。则需要拿出来放在后面
605
+ for (let i = this.virtualScrollX.endIndex; i < this.tableHeaderLast.length; i++) {
606
+ const col = this.tableHeaderLast[i];
607
+ if (col.fixed === 'right') rightCols.push(col);
608
+ }
609
+
610
+ const mainColumns = this.tableHeaderLast.slice(this.virtualScrollX.startIndex, this.virtualScrollX.endIndex);
611
+
612
+ return leftCols.concat(mainColumns).concat(rightCols);
613
+ }
614
+ return this.tableHeaderLast;
615
+ },
616
+ /** 横向虚拟滚动,右边距 */
617
+ virtualX_offsetRight() {
618
+ if (!this.virtualX_on) return 0;
619
+ let width = 0;
620
+ for (let i = this.virtualScrollX.endIndex; i < this.tableHeaderLast.length; i++) {
621
+ const col = this.tableHeaderLast[i];
622
+ width += parseInt(col.width || col.maxWidth || col.minWidth);
623
+ }
624
+ return width;
625
+ },
626
+
627
+ // fixedLeftColWidth() {
628
+ // let fixedLeftColumns = this.tableHeaderLast.filter(it => it.fixed === 'left');
629
+ // let width = 0;
630
+ // for (let i = 0; i < fixedLeftColumns.length; i++) {
631
+ // const col = fixedLeftColumns[i];
632
+ // width += parseInt(col.width);
633
+ // }
634
+ // return width;
635
+ // },
636
+ /** 计算每个fixed:left列前面列的总宽度,fixed:right右边列的总宽度,用于定位 */
637
+ fixedColumnsPositionStore() {
638
+ const store = {};
639
+ const cols = [...this.tableHeaderLast];
640
+ let left = 0;
641
+ for (let i = 0; i < cols.length; i++) {
642
+ const item = cols[i];
643
+ if (item.fixed === 'left') {
644
+ store[item.dataIndex] = left;
645
+ left += parseInt(item.width);
646
+ }
647
+ }
648
+ let right = 0;
649
+ for (let i = cols.length - 1; i >= 0; i--) {
650
+ const item = cols[i];
651
+ if (item.fixed === 'right') {
652
+ store[item.dataIndex] = right;
653
+ right += parseInt(item.width);
654
+ }
655
+ }
656
+
657
+ return store;
658
+ },
659
+ },
660
+ watch: {
661
+ columns: {
662
+ handler() {
663
+ this.dealColumns();
664
+ this.initVirtualScrollX();
665
+ },
666
+ // deep: true, // 不能加,因为this.dealColumns 中操作了this.columns
667
+ },
668
+ /** 监听表格数据变化 */
669
+ dataSource: {
670
+ handler(val) {
671
+ // this.dealColumns(val);
672
+ let initVirtualScrollY = false;
673
+ if (this.dataSourceCopy.length !== val.length) {
674
+ initVirtualScrollY = true;
675
+ }
676
+ this.dataSourceCopy = [...val];
677
+ // 数据长度没变则不计算虚拟滚动
678
+ if (initVirtualScrollY) this.initVirtualScrollY();
679
+
680
+ if (this.sortCol) {
681
+ // 排序
682
+ const column = this.tableHeaderLast.find(it => it.dataIndex === this.sortCol);
683
+ this.onColumnSort(column, false);
684
+ }
685
+ },
686
+ deep: false, // TODO:prop 控制监听
687
+ },
688
+ },
689
+ created() {
690
+ this.dealColumns();
691
+ this.dataSourceCopy = [...this.dataSource];
692
+ },
693
+ mounted() {
694
+ this.initVirtualScroll();
695
+ // 通过wheel 模拟scroll事件,passive:false 使合成器线程等待主线程
696
+ // this.$refs.tableContainer.addEventListener(
697
+ // 'wheel',
698
+ // e => {
699
+ // e.preventDefault();
700
+ // const event = {
701
+ // target: {
702
+ // scrollTop: this.$refs.tableContainer.scrollTop + (e.deltaY > 0 ? 60 : -60),
703
+ // scrollLeft: 0,
704
+ // },
705
+ // };
706
+ // this.onTableScroll(event);
707
+ // this.$refs.tableContainer.scrollTop = event.target.scrollTop < 0 ? 0 : event.target.scrollTop;
708
+ // },
709
+ // { passive: false },
710
+ // );
711
+ this.initColResizeEvent();
712
+ },
713
+ beforeUnmount() {
714
+ this.clearColResizeEvent();
715
+ },
716
+ methods: {
717
+ /**
718
+ * 初始化虚拟滚动参数
719
+ * @param {number} [height] 虚拟滚动的高度
720
+ */
721
+ initVirtualScroll(height) {
722
+ this.initVirtualScrollY(height);
723
+ this.initVirtualScrollX();
724
+ },
725
+ /**
726
+ * 初始化Y虚拟滚动参数
727
+ * @param {number} [height] 虚拟滚动的高度
728
+ */
729
+ initVirtualScrollY(height) {
730
+ if (this.virtual_on) {
731
+ this.virtualScroll.containerHeight =
732
+ typeof height === 'number' ? height : this.$refs.tableContainer?.offsetHeight;
733
+ this.updateVirtualScrollY(this.$refs.tableContainer?.scrollTop);
734
+ // const { offsetTop, containerHeight, rowHeight } = this.virtualScroll;
735
+ // const tableAllHeight = this.dataSourceCopy.length * rowHeight;
736
+ // const overflowHeight = tableAllHeight - containerHeight;
737
+ // if (overflowHeight < offsetTop && overflowHeight > 0) {
738
+ // this.virtualScroll.offsetTop = overflowHeight;
739
+ // this.virtualScroll.startIndex = Math.ceil(overflowHeight / rowHeight);
740
+ // } else if (overflowHeight <= 0) {
741
+ // this.virtualScroll.offsetTop = 0;
742
+ // this.virtualScroll.startIndex = 0;
743
+ // }
744
+ }
745
+ },
746
+ initVirtualScrollX() {
747
+ if (this.virtualX) {
748
+ const { offsetWidth, scrollLeft } = this.$refs.tableContainer || {};
749
+ // this.scrollTo(null, 0);
750
+ this.virtualScrollX.containerWidth = offsetWidth;
751
+ this.updateVirtualScrollX(scrollLeft);
752
+ }
753
+ },
754
+ /** 通过滚动条位置,计算虚拟滚动的参数 */
755
+ updateVirtualScrollY(sTop = 0) {
756
+ const { rowHeight } = this.virtualScroll;
757
+ const startIndex = Math.floor(sTop / rowHeight);
758
+ Object.assign(this.virtualScroll, {
759
+ startIndex,
760
+ offsetTop: startIndex * rowHeight, // startIndex之前的高度
761
+ });
762
+ },
763
+ /** 通过横向滚动条位置,计算横向虚拟滚动的参数 */
764
+ updateVirtualScrollX(sLeft = 0) {
765
+ if (!this.tableHeaderLast?.length) return;
766
+ let startIndex = 0;
767
+ let offsetLeft = 0;
768
+
769
+ let colWidthSum = 0;
770
+ for (let colIndex = 0; colIndex < this.tableHeaderLast.length; colIndex++) {
771
+ startIndex++;
772
+ const col = this.tableHeaderLast[colIndex];
773
+ // fixed left 不进入计算列宽
774
+ if (col.fixed === 'left') continue;
775
+ const colWidth = parseInt(col.width || col.maxWidth || col.minWidth);
776
+ colWidthSum += colWidth;
777
+ // 列宽(非固定列)加到超过scrollLeft的时候,表示startIndex从上一个开始下标
778
+ if (colWidthSum >= sLeft) {
779
+ offsetLeft = colWidthSum - colWidth;
780
+ startIndex--;
781
+ break;
782
+ }
783
+ }
784
+ // -----
785
+ colWidthSum = 0;
786
+ let endIndex = this.tableHeaderLast.length;
787
+ for (let colIndex = startIndex; colIndex < this.tableHeaderLast.length - 1; colIndex++) {
788
+ const col = this.tableHeaderLast[colIndex];
789
+ colWidthSum += parseInt(col.width || col.maxWidth || col.minWidth);
790
+ // 列宽大于容器宽度则停止
791
+ if (colWidthSum >= this.virtualScrollX.containerWidth) {
792
+ endIndex = colIndex + 2; // TODO:预渲染的列数
793
+ break;
794
+ }
795
+ }
796
+ Object.assign(this.virtualScrollX, { startIndex, endIndex, offsetLeft });
797
+ },
798
+ /**
799
+ * 固定列的style
800
+ * @param {1|2} tagType 1-th 2-td
801
+ * @param {StkTableColumn} col
802
+ */
803
+ fixedStyle(tagType, col) {
804
+ const style = {};
805
+ if (_isLegacyMode) {
806
+ if (tagType === 1) {
807
+ style.position = 'relative';
808
+ style.top = this.virtualScroll.scrollTop + 'px';
809
+ }
810
+ }
811
+ const { fixed, dataIndex } = col;
812
+ if (fixed === 'left' || fixed === 'right') {
813
+ const isFixedLeft = fixed === 'left';
814
+ if (_isLegacyMode) {
815
+ /**
816
+ * ----------浏览器兼容--------------
817
+ */
818
+ style.position = 'relative'; // 固定列方案替换为relative。原因:transform 在chrome84浏览器,列变动会导致横向滚动条计算出问题。
819
+ if (isFixedLeft) {
820
+ if (this.virtualX_on) style.left = this.virtualScrollX.scrollLeft - this.virtualScrollX.offsetLeft + 'px';
821
+ else style.left = this.virtualScrollX.scrollLeft + 'px';
822
+ } else {
823
+ // TODO:计算右侧距离
824
+ style.right = `${this.virtualX_offsetRight}px`;
825
+ }
826
+ if (tagType === 1) {
827
+ style.top = this.virtualScroll.scrollTop + 'px';
828
+ style.zIndex = isFixedLeft ? 4 : 3; // 保证固定列高于其他单元格
829
+ } else {
830
+ style.zIndex = isFixedLeft ? 3 : 2;
831
+ }
832
+ } else {
833
+ /**
834
+ * -------------高版本浏览器----------------
835
+ */
836
+ style.position = 'sticky'; // sticky 方案在低版本浏览器不兼容。具体表现为横向滚动超过一个父容器宽度(非table宽度)会导致sticky吸附失效。浏览器bug。
837
+ if (isFixedLeft) {
838
+ style.left = this.fixedColumnsPositionStore[dataIndex] + 'px';
839
+ } else {
840
+ style.right = this.fixedColumnsPositionStore[dataIndex] + 'px';
841
+ }
842
+ if (tagType === 1) {
843
+ style.top = '0';
844
+ style.zIndex = isFixedLeft ? 4 : 3; // 保证固定列高于其他单元格
845
+ } else {
846
+ style.zIndex = isFixedLeft ? 3 : 2;
847
+ }
848
+ }
849
+ }
850
+
851
+ return style;
852
+ },
853
+ /**
854
+ * 处理多级表头
855
+ * FIXME: 仅支持到两级表头。不支持多级。
856
+ */
857
+ dealColumns() {
858
+ // reset
859
+ this.tableHeaders = [];
860
+ this.tableHeaderLast = [];
861
+ const copyColumn = this.columns; // do not deep clone
862
+ const deep = _howDeepTheColumn(copyColumn);
863
+ const tmpHeaderRows = [];
864
+ const tmpHeaderLast = [];
865
+
866
+ // 展开columns
867
+ (function flat(arr, level = 0) {
868
+ const colArr = [];
869
+ const childrenArr = [];
870
+ arr.forEach(col => {
871
+ col.rowSpan = col.children ? false : deep - level;
872
+ col.colSpan = col.children?.length;
873
+ if (col.rowSpan === 1) delete col.rowSpan;
874
+ if (col.colSpan === 1) delete col.colSpan;
875
+ colArr.push(col);
876
+ if (col.children) {
877
+ childrenArr.push(...col.children);
878
+ } else {
879
+ tmpHeaderLast.push(col); // 没有children的列作为colgroup
880
+ }
881
+ });
882
+ tmpHeaderRows.push(colArr);
883
+ if (childrenArr.length) flat(childrenArr, level + 1);
884
+ })(copyColumn);
885
+
886
+ this.tableHeaders = tmpHeaderRows;
887
+ this.tableHeaderLast = tmpHeaderLast;
888
+ },
889
+ /**
890
+ * 行唯一值生成
891
+ */
892
+ rowKeyGen(row) {
893
+ let key = this.rowKeyGenStore.get(row);
894
+ if (!key) {
895
+ key = typeof this.rowKey === 'function' ? this.rowKey(row) : row[this.rowKey];
896
+ this.rowKeyGenStore.set(row, key);
897
+ }
898
+ return key;
899
+ },
900
+ /**
901
+ * 列唯一键
902
+ * @param {StkTableColumn} col
903
+ */
904
+ colKeyGen(col) {
905
+ return typeof this.colKey === 'function' ? this.colKey(col) : col[this.colKey];
906
+ },
907
+ /**
908
+ * 性能优化,缓存style行内样式
909
+ *
910
+ * FIXME: col变化时仍从缓存拿style。watch col?
911
+ * @param {1|2} tagType 1-th 2-td
912
+ * @param {StkTableColumn} col
913
+ */
914
+ getCellStyle(tagType, col) {
915
+ const fixedStyle = this.fixedStyle(tagType, col);
916
+ const style = {
917
+ width: col.width,
918
+ minWidth: this.colResizable ? col.width : col.minWidth || col.width,
919
+ maxWidth: this.colResizable ? col.width : col.maxWidth || col.width,
920
+ ...fixedStyle,
921
+ };
922
+ if (tagType === 1) {
923
+ // TH
924
+ style.textAlign = col.headerAlign;
925
+ } else if (tagType === 2) {
926
+ // TD
927
+ style.textAlign = col.align;
928
+ }
929
+
930
+ return style;
931
+ },
932
+
933
+ //#region ------event handler-------------
934
+ /**
935
+ * 表头点击排序
936
+ * @param {boolean} options.force sort-remote 开启后是否强制排序
937
+ * @param {boolean} options.emit 是否触发回调
938
+ */
939
+ onColumnSort(col, click = true, options = {}) {
940
+ if (!col?.sorter) return;
941
+ options = { force: false, emit: false, ...options };
942
+ if (this.sortCol !== col.dataIndex) {
943
+ // 改变排序的列时,重置排序
944
+ this.sortCol = col.dataIndex;
945
+ this.sortOrderIndex = 0;
946
+ }
947
+ if (click) this.sortOrderIndex++;
948
+ this.sortOrderIndex = this.sortOrderIndex % 3;
949
+
950
+ const order = this.sortSwitchOrder[this.sortOrderIndex];
951
+
952
+ if (!this.sortRemote || options.force) {
953
+ this.dataSourceCopy = tableSort(col, order, this.dataSource);
954
+ }
955
+ // 只有点击才触发事件
956
+ if (click || options.emit) {
957
+ this.$emit('sort-change', col, order, [...this.dataSourceCopy]);
958
+ }
959
+ },
960
+ /** 插入一行
961
+ insertData(data) {
962
+ if(!this.sortCol) return;
963
+ const col = this.columns.find(it => it.dataIndex === this.sortCol);
964
+ const sorter = col.sorter;
965
+ },*/
966
+ onRowClick(e, row) {
967
+ this.$emit('row-click', e, row);
968
+ // 选中同一行不触发current-change 事件
969
+ if (this.currentItem.value === row) return;
970
+ this.currentItem.value = row;
971
+ this.$emit('current-change', e, row);
972
+ },
973
+ onRowDblclick(e, row) {
974
+ this.$emit('row-dblclick', e, row);
975
+ },
976
+ /** 表头行右键 */
977
+ onHeaderMenu(e) {
978
+ this.$emit('header-row-menu', e);
979
+ },
980
+ /** 表体行右键 */
981
+ onRowMenu(e, row) {
982
+ this.$emit('row-menu', e, row);
983
+ },
984
+ /** 单元格单击 */
985
+ onCellClick(e, row, col) {
986
+ this.$emit('cell-click', e, row, col);
987
+ },
988
+ /** 表头单元格单击 */
989
+ onHeaderCellClick(e, col) {
990
+ this.$emit('header-cell-click', e, col);
991
+ },
992
+ /**
993
+ * 鼠标滚轮事件监听
994
+ * @param {MouseEvent} e
995
+ */
996
+ onTableWheel(e) {
997
+ if (this.colResizeState.isResizing) {
998
+ // 正在调整列宽时,不允许用户滚动
999
+ e.preventDefault();
1000
+ e.stopPropagation();
1001
+ return;
1002
+ }
1003
+ },
1004
+ /**
1005
+ * 滚动条监听
1006
+ * @param {MouseEvent} e
1007
+ */
1008
+ onTableScroll(e) {
1009
+ if (!e?.target) return;
1010
+
1011
+ // 此处可优化,因为访问e.target.scrollXX消耗性能
1012
+ const { scrollTop, scrollLeft } = e.target;
1013
+ // 纵向滚动有变化
1014
+ if (scrollTop !== this.virtualScroll.scrollTop) this.virtualScroll.scrollTop = scrollTop;
1015
+ if (this.virtual_on) {
1016
+ this.updateVirtualScrollY(scrollTop);
1017
+ }
1018
+
1019
+ // 横向滚动有变化
1020
+ if (scrollLeft !== this.virtualScrollX.scrollLeft) this.virtualScrollX.scrollLeft = scrollLeft;
1021
+ if (this.virtualX_on) {
1022
+ this.updateVirtualScrollX(scrollLeft);
1023
+ }
1024
+ this.$emit('scroll', e);
1025
+ // this.showFixedLeftShadow = e.target.scrollLeft > 0;
1026
+ },
1027
+ /** tr hover事件 */
1028
+ onTrMouseOver(e, item) {
1029
+ if (this.showTrHoverClass) {
1030
+ this.currentHover.value = this.rowKeyGen(item);
1031
+ }
1032
+ },
1033
+ /** th拖动释放时 */
1034
+ onThDrop(e) {
1035
+ let th = e.target;
1036
+ // 找到th元素
1037
+ while (th) {
1038
+ if (th.tagName === 'TH') break;
1039
+ th = th.parentNode;
1040
+ }
1041
+ // const i = Array.prototype.indexOf.call(th.parentNode.children, th); // 得到是第几个子元素
1042
+ if (this.thDrag.dragStartKey !== th.dataset.colKey) {
1043
+ this.$emit('col-order-change', this.thDrag.dragStartKey, th.dataset.colKey);
1044
+ }
1045
+ this.$emit('th-drop', th.dataset.colKey);
1046
+ },
1047
+ /** 开始拖动记录th位置 */
1048
+ onThDragStart(e) {
1049
+ // const i = Array.prototype.indexOf.call(e.target.parentNode.children, e.target); // 得到是第几个子元素
1050
+ this.thDrag.dragStartKey = e.target.dataset.colKey;
1051
+ this.$emit('th-drag-start', this.thDrag.dragStartKey);
1052
+ },
1053
+ onThDragOver(e) {
1054
+ e.preventDefault();
1055
+ },
1056
+ /** 初始化列宽拖动事件 */
1057
+ initColResizeEvent() {
1058
+ window.addEventListener('mousemove', this.onThResizeMouseMove);
1059
+ window.addEventListener('mouseup', this.onThResizeMouseUp);
1060
+ },
1061
+ /** 清除列宽拖动事件 */
1062
+ clearColResizeEvent() {
1063
+ window.removeEventListener('mousemove', this.onThResizeMouseMove);
1064
+ window.removeEventListener('mouseup', this.onThResizeMouseUp);
1065
+ },
1066
+ /**
1067
+ * 拖动开始
1068
+ * @param {MouseEvent} e
1069
+ * @param {1|2} lr left or right handle. 1-left 2-right
1070
+ * @param {StkTableColumn} col 当前列配置
1071
+ * @param {boolean} isPrev 是否要上一列
1072
+ */
1073
+ onThResizeMouseDown(e, col, isPrev) {
1074
+ e.stopPropagation();
1075
+ e.preventDefault();
1076
+ const { clientX } = e;
1077
+ const { scrollLeft } = this.$refs.tableContainer;
1078
+ const { left } = this.$refs.tableContainer.getBoundingClientRect();
1079
+ /** 列下标 */
1080
+ let colIndex = this.tableHeaderLast.findIndex(it => this.colKeyGen(it) === this.colKeyGen(col));
1081
+ if (isPrev) {
1082
+ // 上一列
1083
+ colIndex -= 1;
1084
+ col = this.tableHeaderLast[colIndex];
1085
+ }
1086
+ const offsetTableX = clientX - left + scrollLeft;
1087
+
1088
+ // 记录拖动状态
1089
+ Object.assign(this.colResizeState, {
1090
+ isResizing: true,
1091
+ currentCol: col,
1092
+ currentColIndex: colIndex,
1093
+ startPageX: clientX,
1094
+ startOffsetTableX: offsetTableX,
1095
+ });
1096
+
1097
+ // 展示指示线,更新其位置
1098
+ this.$refs.colResizeIndicator.style.display = 'block';
1099
+ this.$refs.colResizeIndicator.style.left = offsetTableX + 'px';
1100
+ this.$refs.colResizeIndicator.style.top = this.$refs.tableContainer.scrollTop + 'px';
1101
+ },
1102
+ /**
1103
+ * @param {MouseEvent} e
1104
+ */
1105
+ onThResizeMouseMove(e) {
1106
+ const { isResizing, currentCol, startX, startOffsetTableX } = this.colResizeState;
1107
+ if (!isResizing) return;
1108
+ const { clientX } = e;
1109
+ e.stopPropagation();
1110
+ e.preventDefault();
1111
+ let moveX = clientX - startX;
1112
+ // 移动量不小于最小列宽
1113
+ if (parseInt(currentCol.width) + moveX < this.colMinWidth) moveX = -parseInt(currentCol.width);
1114
+
1115
+ const offsetTableX = startOffsetTableX + moveX;
1116
+ this.$refs.colResizeIndicator.style.left = offsetTableX + 'px';
1117
+ },
1118
+ /**
1119
+ * @param {MouseEvent} e
1120
+ */
1121
+ onThResizeMouseUp(e) {
1122
+ const { isResizing, startX, currentCol } = this.colResizeState;
1123
+ if (!isResizing) return;
1124
+ const { clientX } = e;
1125
+ const moveX = clientX - startX;
1126
+
1127
+ // 移动量不小于最小列宽
1128
+ let width = parseInt(currentCol.width) + moveX;
1129
+ if (width < this.colMinWidth) width = this.colMinWidth;
1130
+
1131
+ const curCol = this.tableHeaderLast.find(it => this.colKeyGen(it) === this.colKeyGen(currentCol));
1132
+ curCol.width = width + 'px';
1133
+
1134
+ this.$emit('update:columns', [...this.columns]);
1135
+
1136
+ // 隐藏指示线
1137
+ this.$refs.colResizeIndicator.style.display = 'none';
1138
+ // 清除拖动状态
1139
+ this.colResizeState = {
1140
+ isResizing: false,
1141
+ currentCol: null,
1142
+ currentColIndex: 0,
1143
+ startX: 0,
1144
+ scrollLeft: 0,
1145
+ };
1146
+ },
1147
+
1148
+ // ---tool func
1149
+ /**
1150
+ * 计算高亮渐暗颜色的循环
1151
+ * FIXME: 相同数据源,相同引用的情况,将颜色值挂在数据源对象上,在多个表格使用相同数据源时会出问题。
1152
+ */
1153
+ calcHighlightLoop() {
1154
+ if (this.calcHighlightDimLoop) return;
1155
+ this.calcHighlightDimLoop = true;
1156
+ // js计算gradient
1157
+ // raf 太频繁。考虑setTimeout分段设置颜色,过渡靠css transition 补间
1158
+ const recursion = () => {
1159
+ window.setTimeout(() => {
1160
+ const highlightRows = [...this.highlightDimRows];
1161
+ const nowTs = Date.now();
1162
+ for (let i = 0; i < highlightRows.length; i++) {
1163
+ const row = highlightRows[i];
1164
+ // const rowKeyValue = this.rowKeyGen(row);
1165
+ // /**@type {HTMLElement} */
1166
+ // const rowEl = this.$el.querySelector(`[data-row-key="${rowKeyValue}"]`);
1167
+ // if (row._bgc_progress > 0 && rowEl) {
1168
+ // // 开始css transition 补间
1169
+ // if (rowEl.classList.contains('highlight-row-transition')) {
1170
+ // rowEl.classList.remove('highlight-row-transition');
1171
+ // void rowEl.offsetHeight; // reflow
1172
+ // }
1173
+ // rowEl.classList.add('highlight-row-transition');
1174
+ // }
1175
+ // 经过的时间 ÷ 2s 计算出 颜色过渡进度 (0-1)
1176
+ const progress = (nowTs - row._bgc_progress_ms) / _highlightDuration;
1177
+ // row._bgc_progress = progress;
1178
+ if (progress <= 1) {
1179
+ row._bgc = this.highlightInter(progress);
1180
+ } else {
1181
+ row._bgc = ''; // 清空颜色
1182
+ highlightRows.splice(i--, 1);
1183
+ // rowEl.classList.remove('highlight-row-transition');
1184
+ }
1185
+ }
1186
+ this.highlightDimRows = new Set(highlightRows);
1187
+
1188
+ if (this.highlightDimRows.size > 0) {
1189
+ // 还有高亮的行,则下一次循环
1190
+ recursion();
1191
+ } else {
1192
+ // 没有则停止循环
1193
+ this.calcHighlightDimLoop = false;
1194
+ }
1195
+ }, _highlightColorChangeFreq);
1196
+ };
1197
+ recursion();
1198
+ },
1199
+ //#endregion ------event handler-------------
1200
+ //#region ---- ref function-----
1201
+ /**
1202
+ * 选中一行,
1203
+ * @param {string} rowKey
1204
+ * @param {boolean} option.silent 是否触发回调
1205
+ */
1206
+ setCurrentRow(rowKey, option = { silent: false }) {
1207
+ if (!this.dataSourceCopy.length) return;
1208
+ this.currentItem.value = this.dataSourceCopy.find(it => this.rowKeyGen(it) === rowKey);
1209
+ if (!option.silent) {
1210
+ this.$emit('current-change', this.currentItem);
1211
+ }
1212
+ },
1213
+ /** 高亮一个单元格 */
1214
+ setHighlightDimCell(rowKeyValue, dataIndex) {
1215
+ // TODO: 支持动态计算高亮颜色。不易实现。需记录每一个单元格的颜色情况。
1216
+ const cellEl = this.$el.querySelector(`[data-row-key="${rowKeyValue}"]>[data-index="${dataIndex}"]`);
1217
+ if (!cellEl) return;
1218
+ if (cellEl.classList.contains('highlight-cell')) {
1219
+ cellEl.classList.remove('highlight-cell');
1220
+ void cellEl.offsetHeight; // 通知浏览器重绘
1221
+ }
1222
+ cellEl.classList.add('highlight-cell');
1223
+ window.clearTimeout(this.highlightDimCellsTimeout.get(rowKeyValue));
1224
+ this.highlightDimCellsTimeout.set(
1225
+ rowKeyValue,
1226
+ window.setTimeout(() => {
1227
+ cellEl.classList.remove('highlight-cell');
1228
+ this.highlightDimCellsTimeout.delete(rowKeyValue);
1229
+ }, _highlightDuration),
1230
+ );
1231
+ },
1232
+ /**
1233
+ * 高亮一行
1234
+ * @param {Array<string|number>} rowKeyValues
1235
+ */
1236
+ setHighlightDimRow(rowKeyValues) {
1237
+ if (!Array.isArray(rowKeyValues)) rowKeyValues = [rowKeyValues];
1238
+ if (this.virtual) {
1239
+ // --------虚拟滚动用js计算颜色渐变的高亮方案
1240
+ const nowTs = Date.now(); // 重置渐变进度
1241
+ for (let i = 0; i < rowKeyValues.length; i++) {
1242
+ const rowKeyValue = rowKeyValues[i];
1243
+ const row = this.dataSource.find(it => this.rowKeyGen(it) === rowKeyValue);
1244
+ if (!row) continue;
1245
+ row._bgc_progress_ms = nowTs;
1246
+ // row._bgc_progress = 0;
1247
+ this.highlightDimRows.add(row);
1248
+ }
1249
+ this.calcHighlightLoop();
1250
+ } else {
1251
+ // -------- 普通滚动用css @keyframes动画,实现高亮
1252
+ /**是否需要重绘 */
1253
+ let needRepaint = false;
1254
+ /** @type {HTMLElement[]} */
1255
+ const rowElTemp = [];
1256
+ for (let i = 0; i < rowKeyValues.length; i++) {
1257
+ const rowKeyValue = rowKeyValues[i];
1258
+ /**@type {HTMLElement|null} */
1259
+ const rowEl = this.$el.querySelector(`[data-row-key="${rowKeyValue}"]`);
1260
+ if (!rowEl) continue;
1261
+ if (rowEl.classList.contains('highlight-row')) {
1262
+ rowEl.classList.remove('highlight-row');
1263
+ needRepaint = true;
1264
+ }
1265
+ rowElTemp.push(rowEl);
1266
+ // 动画结束移除class
1267
+ window.clearTimeout(this.highlightDimRowsTimeout.get(rowKeyValue));
1268
+ this.highlightDimRowsTimeout.set(
1269
+ rowKeyValue,
1270
+ window.setTimeout(() => {
1271
+ rowEl.classList.remove('highlight-row');
1272
+ this.highlightDimRowsTimeout.delete(rowKeyValue); // 回收内存
1273
+ }, _highlightDuration),
1274
+ );
1275
+ }
1276
+ if (needRepaint) {
1277
+ void this.$el.offsetWidth; //强制浏览器重绘
1278
+ }
1279
+ rowElTemp.forEach(el => el.classList.add('highlight-row')); // 统一添加动画
1280
+ }
1281
+ },
1282
+ /**
1283
+ * 设置表头排序状态
1284
+ * @param {string} dataIndex 列字段
1285
+ * @param {'asc'|'desc'|null} order
1286
+ * @param {object} option.sortOption 指定排序参数
1287
+ * @param {boolean} option.silent 是否触发回调
1288
+ * @param {boolean} option.sort 是否排序
1289
+ */
1290
+ setSorter(dataIndex, order, option = {}) {
1291
+ option = { silent: true, sortOption: null, sort: true, ...option };
1292
+ this.sortCol = dataIndex;
1293
+ this.sortOrderIndex = this.sortSwitchOrder.findIndex(it => it === order);
1294
+ if (option.sort && this.dataSourceCopy?.length) {
1295
+ // 如果表格有数据,则进行排序
1296
+ const column = option.sortOption || this.tableHeaderLast.find(it => it.dataIndex === this.sortCol);
1297
+ if (column) this.onColumnSort(column, false, { force: true, emit: !option.silent });
1298
+ else console.warn('Can not find column by dataIndex:', this.sortCol);
1299
+ }
1300
+ return this.dataSourceCopy;
1301
+ },
1302
+ /** 重置排序 */
1303
+ resetSorter() {
1304
+ this.sortCol = null;
1305
+ this.sortOrderIndex = 0;
1306
+ this.dataSourceCopy = [...this.dataSource];
1307
+ },
1308
+ /** 滚动 */
1309
+ scrollTo(top = 0, left = 0) {
1310
+ if (top !== null) this.$refs.tableContainer.scrollTop = top;
1311
+ if (left !== null) this.$refs.tableContainer.scrollLeft = left;
1312
+ },
1313
+
1314
+ /** 获取当前状态的表格数据 */
1315
+ getTableData() {
1316
+ return [...this.dataSourceCopy];
1317
+ },
1318
+ //#endregion ---ref function
1319
+ },
1320
+ });
1321
+ </script>
1322
+
1323
+ <style lang="less" scoped>
1324
+ .stk-table {
1325
+ // contain: strict;
1326
+ --row-height: 28px;
1327
+ --cell-padding-x: 8px;
1328
+ --resize-handle-width: 4px;
1329
+ --border-color: #e8eaec;
1330
+ --border-width: 1px;
1331
+ --td-bgc: #fff;
1332
+ --th-bgc: #f8f8f9;
1333
+ --tr-active-bgc: rgb(230, 247, 255);
1334
+ --tr-hover-bgc: rgba(230, 247, 255, 0.7);
1335
+ --bg-border-top: linear-gradient(180deg, var(--border-color) var(--border-width), transparent var(--border-width));
1336
+ --bg-border-right: linear-gradient(270deg, var(--border-color) var(--border-width), transparent var(--border-width));
1337
+ --bg-border-bottom: linear-gradient(0deg, var(--border-color) var(--border-width), transparent var(--border-width));
1338
+ --bg-border-left: linear-gradient(90deg, var(--border-color) var(--border-width), transparent var(--border-width));
1339
+ --highlight-color: #71a2fd;
1340
+
1341
+ --sort-arrow-color: #5d5f69;
1342
+ --sort-arrow-hover-color: #8f90b5;
1343
+ --sort-arrow-active-color: #1b63d9;
1344
+ --sort-arrow-active-sub-color: #cbcbe1;
1345
+ /** 列宽拖动指示器颜色 */
1346
+ --col-resize-indicator-color: #cbcbe1;
1347
+
1348
+ position: relative;
1349
+ overflow: auto;
1350
+ display: flex;
1351
+ flex-direction: column;
1352
+
1353
+ /**深色模式 */
1354
+ &.dark {
1355
+ --th-bgc: #181c21;
1356
+ --td-bgc: #181c21;
1357
+ --border-color: #26292e;
1358
+ --tr-active-bgc: #283f63;
1359
+ --tr-hover-bgc: #1a2b46;
1360
+ --table-bgc: #181c21;
1361
+ --highlight-color: #1e4c99; // 不能用rgba,因为固定列时,会变成半透明
1362
+
1363
+ --sort-arrow-color: #5d6064;
1364
+ --sort-arrow-hover-color: #727782;
1365
+ --sort-arrow-active-color: #d0d1d2;
1366
+ --sort-arrow-active-sub-color: #5d6064;
1367
+
1368
+ --col-resize-indicator-color: #5d6064;
1369
+
1370
+ // background-color: var(--table-bgc); // ⭐这里加background-color会导致表格出滚动白屏
1371
+ color: #d0d1d2;
1372
+ }
1373
+
1374
+ // .stk-table-fixed-left-col-box-shadow {
1375
+ // position: sticky;
1376
+ // left: 0;
1377
+ // top: 0;
1378
+ // height: 100%;
1379
+ // box-shadow: 0 0 10px;
1380
+ // z-index: 1;
1381
+ // pointer-events: none;
1382
+ // }
1383
+ &.headless {
1384
+ border-top: 1px solid var(--border-color);
1385
+ }
1386
+
1387
+ /** 调整列宽的时候不要触发th事件。否则会导致触发表头点击排序 */
1388
+ &.is-col-resizing th {
1389
+ pointer-events: none;
1390
+ }
1391
+
1392
+ /** 列宽调整指示器 */
1393
+ .column-resize-indicator {
1394
+ width: 0;
1395
+ height: 100%;
1396
+ border-left: 1px dashed var(--col-resize-indicator-color);
1397
+ position: absolute;
1398
+ z-index: 10;
1399
+ display: none;
1400
+ pointer-events: none;
1401
+ }
1402
+
1403
+ .stk-table-main {
1404
+ border-spacing: 0;
1405
+ border-collapse: separate;
1406
+
1407
+ th,
1408
+ td {
1409
+ z-index: 1;
1410
+ height: var(--row-height);
1411
+ font-size: 14px;
1412
+ box-sizing: border-box;
1413
+ padding: 0 var(--cell-padding-x);
1414
+ background-image: var(--bg-border-right), var(--bg-border-bottom);
1415
+ }
1416
+
1417
+ thead {
1418
+ tr {
1419
+ &:first-child th {
1420
+ position: sticky;
1421
+ top: 0;
1422
+ // border-top: 1px solid var(--border-color);
1423
+ background-image: var(--bg-border-top), var(--bg-border-right), var(--bg-border-bottom);
1424
+
1425
+ &:first-child {
1426
+ background-image: var(--bg-border-top), var(--bg-border-right), var(--bg-border-bottom),
1427
+ var(--bg-border-left);
1428
+ }
1429
+ }
1430
+
1431
+ th {
1432
+ background-color: var(--th-bgc);
1433
+
1434
+ &.sortable {
1435
+ cursor: pointer;
1436
+ }
1437
+
1438
+ &:first-child {
1439
+ // border-left: 1px solid var(--border-color);
1440
+ background-image: var(--bg-border-right), var(--bg-border-bottom), var(--bg-border-left);
1441
+ // padding-left: 12px;
1442
+ }
1443
+
1444
+ // &:last-child {
1445
+ // padding-right: 12px;
1446
+ // }
1447
+ &.text-overflow {
1448
+ .table-header-cell-wrapper {
1449
+ white-space: nowrap;
1450
+ overflow: hidden;
1451
+
1452
+ .table-header-title {
1453
+ text-overflow: ellipsis;
1454
+ overflow: hidden;
1455
+ }
1456
+ }
1457
+ }
1458
+
1459
+ &:not(.sorter-desc):not(.sorter-asc):hover .table-header-cell-wrapper .table-header-sorter {
1460
+ #arrow-up {
1461
+ fill: var(--sort-arrow-hover-color);
1462
+ }
1463
+
1464
+ #arrow-down {
1465
+ fill: var(--sort-arrow-hover-color);
1466
+ }
1467
+ }
1468
+
1469
+ &.sorter-desc .table-header-cell-wrapper .table-header-sorter {
1470
+ // display:initial;
1471
+ #arrow-up {
1472
+ fill: var(--sort-arrow-active-sub-color);
1473
+ }
1474
+
1475
+ #arrow-down {
1476
+ fill: var(--sort-arrow-active-color);
1477
+ }
1478
+ }
1479
+
1480
+ &.sorter-asc .table-header-cell-wrapper .table-header-sorter {
1481
+ // display:initial;
1482
+ #arrow-up {
1483
+ fill: var(--sort-arrow-active-color);
1484
+ }
1485
+
1486
+ #arrow-down {
1487
+ fill: var(--sort-arrow-active-sub-color);
1488
+ }
1489
+ }
1490
+
1491
+ .table-header-cell-wrapper {
1492
+ max-width: 100%; //最大宽度不超过列宽
1493
+ display: inline-flex;
1494
+ align-items: center;
1495
+
1496
+ .table-header-title {
1497
+ overflow: hidden;
1498
+ align-self: flex-start;
1499
+ }
1500
+
1501
+ .table-header-sorter {
1502
+ flex-shrink: 0;
1503
+ margin-left: 4px;
1504
+ width: 16px;
1505
+ height: 16px;
1506
+ // display:none;
1507
+ #arrow-up,
1508
+ #arrow-down {
1509
+ fill: var(--sort-arrow-color);
1510
+ }
1511
+ }
1512
+ .table-header-resizer {
1513
+ position: absolute;
1514
+ top: 0;
1515
+ cursor: col-resize;
1516
+ width: var(--resize-handle-width);
1517
+ height: var(--row-height);
1518
+ &.left {
1519
+ left: 0;
1520
+ }
1521
+ &.right {
1522
+ right: 0;
1523
+ }
1524
+ }
1525
+ }
1526
+ }
1527
+ }
1528
+ }
1529
+
1530
+ tbody {
1531
+ /**高亮渐暗 */
1532
+ @keyframes dim {
1533
+ from {
1534
+ background-color: var(--highlight-color);
1535
+ }
1536
+ }
1537
+
1538
+ tr {
1539
+ background-color: var(--td-bgc); // td inherit tr bgc
1540
+
1541
+ &.highlight-row {
1542
+ animation: dim 2s linear;
1543
+ }
1544
+
1545
+ // &.highlight-row-transition {
1546
+ // transition: background-color 0.2s linear;
1547
+ // }
1548
+
1549
+ &.hover,
1550
+ &:hover {
1551
+ background-color: var(--tr-hover-bgc);
1552
+ }
1553
+
1554
+ &.active {
1555
+ background-color: var(--tr-active-bgc);
1556
+ }
1557
+
1558
+ td {
1559
+ &.fixed-cell {
1560
+ background-color: inherit; // 防止横向滚动后透明
1561
+ }
1562
+
1563
+ &:first-child {
1564
+ background-image: var(--bg-border-right), var(--bg-border-bottom), var(--bg-border-left);
1565
+ }
1566
+
1567
+ &.highlight-cell {
1568
+ animation: dim 2s linear;
1569
+ }
1570
+
1571
+ &.text-overflow {
1572
+ .table-cell-wrapper {
1573
+ white-space: nowrap;
1574
+ overflow: hidden;
1575
+ text-overflow: ellipsis;
1576
+ }
1577
+ }
1578
+
1579
+ // &.perch-td {
1580
+ // padding: 0;
1581
+ // height: 0;
1582
+ // &.top {
1583
+ // background-image: repeating-linear-gradient(
1584
+ // 180deg,
1585
+ // transparent 0,
1586
+ // transparent var(--row-height),
1587
+ // var(--border-color) var(--row-height),
1588
+ // var(--border-color) calc(var(--row-height) + 1px)
1589
+ // ),
1590
+ // var(--bg-border-right);
1591
+ // }
1592
+ // &.bottom {
1593
+ // background-image: repeating-linear-gradient(
1594
+ // 0deg,
1595
+ // transparent 0,
1596
+ // transparent var(--row-height),
1597
+ // var(--border-color) var(--row-height),
1598
+ // var(--border-color) calc(var(--row-height) + 1px)
1599
+ // ),
1600
+ // var(--bg-border-right);
1601
+ // }
1602
+ // }
1603
+ }
1604
+ }
1605
+
1606
+ // 斑马纹
1607
+ // tr:nth-child(2n) td {
1608
+ // background-color: #fafafc;
1609
+ // }
1610
+ // tr:hover {
1611
+ // background-color: #ebf3ff;
1612
+ // }
1613
+ }
1614
+ }
1615
+
1616
+ .stk-table-no-data {
1617
+ background-color: var(--table-bgc);
1618
+ line-height: var(--row-height);
1619
+ text-align: center;
1620
+ font-size: 14px;
1621
+ position: sticky;
1622
+ left: 0px;
1623
+ border-left: var(--border-width) solid var(--border-color);
1624
+ border-right: var(--border-width) solid var(--border-color);
1625
+ border-bottom: var(--border-width) solid var(--border-color);
1626
+ display: flex;
1627
+ flex-direction: column;
1628
+ align-items: center;
1629
+ justify-content: center;
1630
+
1631
+ &.no-data-full {
1632
+ flex: 1;
1633
+ }
1634
+ }
1635
+
1636
+ /**虚拟滚动模式 */
1637
+ &.virtual {
1638
+ .stk-table-main {
1639
+ thead {
1640
+ tr {
1641
+ th {
1642
+ // 为不影响布局,表头行高要定死
1643
+ .table-header-cell-wrapper {
1644
+ overflow: hidden;
1645
+ max-height: var(--row-height);
1646
+ }
1647
+ }
1648
+ }
1649
+ }
1650
+
1651
+ tbody {
1652
+ position: relative;
1653
+
1654
+ tr {
1655
+ td {
1656
+ height: var(--row-height);
1657
+ line-height: 1;
1658
+
1659
+ .table-cell-wrapper {
1660
+ max-height: var(--row-height);
1661
+ overflow: hidden;
1662
+ }
1663
+ }
1664
+ }
1665
+ }
1666
+ }
1667
+ }
1668
+
1669
+ &.virtual-x {
1670
+ .stk-table-main {
1671
+ .virtual-x-left {
1672
+ padding: 0;
1673
+ }
1674
+
1675
+ thead tr:first-child .virtual-x-left + th {
1676
+ // 横向虚拟滚动时,左侧第一个单元格加上border-left
1677
+ background-image: var(--bg-border-top), var(--bg-border-right), var(--bg-border-bottom), var(--bg-border-left);
1678
+ }
1679
+
1680
+ tr .virtual-x-left + th {
1681
+ background-image: var(--bg-border-right), var(--bg-border-bottom), var(--bg-border-left);
1682
+ }
1683
+ }
1684
+ }
1685
+ }
1686
+ </style>