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,590 @@
1
+ <template>
2
+ <div class="stk-table-wrapper dark" @scroll="onTableScroll">
3
+ <table
4
+ v-for="tp in ['main', 'fl']"
5
+ :key="tp"
6
+ :ref="tp === 'fl' && 'fixedLeftTable'"
7
+ class="stk-table"
8
+ :class="{ 'fixed-left': tp === 'fl' }"
9
+ :style="tp === 'main' && { minWidth: minWidth }"
10
+ >
11
+ <thead>
12
+ <tr v-for="(row, index) in tableHeaders" :key="index">
13
+ <template v-for="(col, i) in row">
14
+ <th
15
+ v-if="tp === 'main' || (tp === 'fl' && col.fixed === 'left')"
16
+ :key="i"
17
+ :rowspan="col.rowSpan"
18
+ :colspan="col.colSpan"
19
+ :style="{
20
+ textAlign: col.headerAlign,
21
+ width: col.width || 'auto',
22
+ minWidth: col.fixed ? col.width : col.minWidth,
23
+ maxWidth: col.fixed ? col.width : col.maxWidth,
24
+ zIndex: 1,
25
+ }"
26
+ :class="{ sortable: col.sorter }"
27
+ @click="onColumnSort(col)"
28
+ >
29
+ <div class="table-header-cell-wrapper">
30
+ <component :is="col.customHeaderCell(col)" v-if="col.customHeaderCell" />
31
+ <template v-else>
32
+ <slot name="table-header" :column="col">
33
+ <span class="table-header-title">{{ col.title }}</span>
34
+ </slot>
35
+ </template>
36
+
37
+ <!-- 排序图图标 -->
38
+ <span
39
+ v-if="col.sorter"
40
+ class="table-header-sorter"
41
+ :class="col.dataIndex === sortCol && 'sorter-' + sortSwitchOrder[sortOrderIndex]"
42
+ >
43
+ <svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 16 16">
44
+ <g id="sort-btn" fill-rule="nonzero">
45
+ <rect id="rect" opacity="0" x="0" y="0" width="16" height="16"></rect>
46
+ <polygon
47
+ id="arrow-up"
48
+ fill="#757699"
49
+ points="7.99693049 2.00077299 4.79705419 6.00077299 11.1722317 6.00077299"
50
+ ></polygon>
51
+ <polygon
52
+ id="arrow-down"
53
+ fill="#757699"
54
+ transform="translate(7.984643, 11.999227) scale(-1, 1) rotate(-180.000000) translate(-7.984643, -11.999227) "
55
+ points="7.99693049 9.999227 4.79705419 13.999227 11.1722317 13.999227"
56
+ ></polygon>
57
+ </g>
58
+ </svg>
59
+ </span>
60
+ </div>
61
+ </th>
62
+ </template>
63
+ </tr>
64
+ </thead>
65
+
66
+ <tbody>
67
+ <template v-if="dataSourceCopy && dataSourceCopy.length">
68
+ <tr
69
+ v-for="(item, i) in dataSourceCopy"
70
+ :key="rowKey ? item[rowKey] : i"
71
+ :data-row-key="rowKey ? item[rowKey] : i"
72
+ :class="{
73
+ active: rowKey ? item[rowKey] === (currentItem && currentItem[rowKey]) : item === currentItem,
74
+ 'row-hover': currentHoverItem === item,
75
+ }"
76
+ @click="onRowClick(item)"
77
+ @dblclick="onRowDblclick(item)"
78
+ @mouseover="currentHoverItem = item"
79
+ @mouseleave="currentHoverItem = null"
80
+ >
81
+ <template v-for="col in tableProps">
82
+ <td
83
+ v-if="tp === 'main' || (tp === 'fl' && col.fixed === 'left')"
84
+ :key="col.dataIndex"
85
+ :data-index="col.dataIndex"
86
+ :style="{
87
+ textAlign: col.align,
88
+ maxWidth: col.fixed ? col.width : col.maxWidth,
89
+ textOverflow: col.textOverflow && 'ellipsis',
90
+ overflow: col.textOverflow && 'hidden',
91
+ }"
92
+ :title="col.textOverflow === 'title' ? item[col.dataIndex] : undefined"
93
+ >
94
+ <template v-if="(tp === 'main' && !col.fixed) || tp === 'fl'">
95
+ <component :is="col.customCell(col, item)" v-if="col.customCell" />
96
+ <span v-else> {{ item[col.dataIndex] ?? emptyCellText }} </span>
97
+ </template>
98
+ </td>
99
+ </template>
100
+ </tr>
101
+ </template>
102
+ </tbody>
103
+ </table>
104
+ <div
105
+ v-if="!dataSourceCopy || !dataSourceCopy.length"
106
+ class="stk-table-no-data"
107
+ :class="{ 'no-data-full': noDataFull }"
108
+ >
109
+ <slot name="no-data">暂无数据</slot>
110
+ </div>
111
+ </div>
112
+ </template>
113
+
114
+ <script>
115
+ /**
116
+ * 此版本用于兼容低版本,左侧固定列问题。
117
+ * 存在的问题:column.dataIndex 作为唯一键,不能重复
118
+ * 改变fixedTable左侧距离方案,可能导致抖动。考虑使用fixed
119
+ * 可优化:没有固定列,可不渲染固定表格
120
+ *
121
+ */
122
+ function _howDeepTheColumn(arr, level = 1) {
123
+ let levels = [level];
124
+ arr.forEach(item => {
125
+ if (item.children?.length) {
126
+ levels.push(_howDeepTheColumn(item.children, level + 1));
127
+ }
128
+ });
129
+ return Math.max(...levels);
130
+ }
131
+
132
+ export default {
133
+ props: {
134
+ minWidth: {
135
+ type: String,
136
+ default: '100%',
137
+ },
138
+ columns: {
139
+ type: Array,
140
+ default: () => [],
141
+ },
142
+ dataSource: {
143
+ type: Array,
144
+ default: () => [],
145
+ },
146
+ rowKey: {
147
+ type: String,
148
+ default: '',
149
+ },
150
+ emptyCellText: {
151
+ type: String,
152
+ default: '--',
153
+ },
154
+ /** 暂无数据兜底高度是否撑满 */
155
+ noDataFull: {
156
+ type: Boolean,
157
+ default: false,
158
+ },
159
+ },
160
+ data() {
161
+ return {
162
+ /** 是否展示横向滚动固定列的阴影 */
163
+ showFixedLeftShadow: false,
164
+
165
+ /** 当前选中的一行*/
166
+ currentItem: {},
167
+ /** 当前hover的行 */
168
+ currentHoverItem: null,
169
+ /** 排序的列*/
170
+ sortCol: null,
171
+ sortOrderIndex: 0,
172
+ /** 排序切换顺序 */
173
+ sortSwitchOrder: [null, 'desc', 'asc'],
174
+ tableHeaders: [],
175
+ /** 若有多级表头时,的tableHeaders */
176
+ tableProps: [],
177
+ dataSourceCopy: [],
178
+ /** 高亮后渐暗的单元格 */
179
+ // highlightDimCells: {},
180
+ /** 高亮后渐暗的行定时器 */
181
+ highlightDimRowsTimeout: new Map(),
182
+ };
183
+ },
184
+ computed: {},
185
+ watch: {
186
+ columns: {
187
+ handler(val) {
188
+ this.dealColumns(val);
189
+ },
190
+ deep: true,
191
+ },
192
+ /** 监听表格数据变化 */
193
+ dataSource(val) {
194
+ // this.dealColumns(val);
195
+ this.dataSourceCopy = [...val];
196
+ if (this.sortCol) {
197
+ // 排序
198
+ const column = this.columns.find(it => it.dataIndex === this.sortCol);
199
+ this.onColumnSort(column, false);
200
+ }
201
+ },
202
+ },
203
+ created() {
204
+ this.dealColumns();
205
+ this.dataSourceCopy = [...this.dataSource];
206
+ },
207
+ mounted() {},
208
+ methods: {
209
+ dealColumns() {
210
+ // reset
211
+ this.tableHeaders = [];
212
+ this.tableProps = [];
213
+ let copyColumn = this.columns;
214
+ let deep = _howDeepTheColumn(copyColumn);
215
+ const tmpHeader = [];
216
+ const tmpProps = [];
217
+ // 展开columns
218
+ (function flat(arr, level = 0) {
219
+ let colArr = [];
220
+ let childrenArr = [];
221
+ arr.forEach(col => {
222
+ col.rowSpan = col.children ? false : deep - level;
223
+ col.colSpan = col.children?.length;
224
+ colArr.push(col);
225
+ if (col.children) {
226
+ childrenArr.push(...col.children);
227
+ } else {
228
+ tmpProps.push(col); // 没有children的组合作为colgroup
229
+ }
230
+ });
231
+ tmpHeader.push(colArr);
232
+ if (childrenArr.length) flat(childrenArr, level + 1);
233
+ })(copyColumn);
234
+ this.tableHeaders = tmpHeader;
235
+ this.tableProps = tmpProps;
236
+ },
237
+ /** 表头点击排序 */
238
+ onColumnSort(col, click = true) {
239
+ if (!col.sorter) return;
240
+ if (this.sortCol !== col.dataIndex) {
241
+ // 改变排序的列时,重置排序
242
+ this.sortCol = col.dataIndex;
243
+ this.sortOrderIndex = 0;
244
+ }
245
+ if (click) {
246
+ this.sortOrderIndex++;
247
+ }
248
+ if (this.sortOrderIndex > 2) this.sortOrderIndex = 0;
249
+ const order = this.sortSwitchOrder[this.sortOrderIndex];
250
+ if (typeof col.sorter === 'function') {
251
+ const customSorterData = col.sorter([...this.dataSource], { order, column: col });
252
+ if (customSorterData) this.dataSourceCopy = customSorterData;
253
+ else this.dataSourceCopy = [...this.dataSource]; // 还原数组
254
+ } else if (order) {
255
+ if (col.sortType === 'number') {
256
+ // 按数字类型排序
257
+ const nanArr = []; // 非数字
258
+ const numArr = []; // 数字
259
+ // 非数字不进入排序,一直排在最后
260
+ for (let i = 0; i < this.dataSourceCopy.length; i++) {
261
+ const row = this.dataSourceCopy[i];
262
+ if (
263
+ row[col.dataIndex] === null ||
264
+ row[col.dataIndex] === '' ||
265
+ typeof row[col.dataIndex] === 'boolean' ||
266
+ Number.isNaN(+row[col.dataIndex])
267
+ ) {
268
+ nanArr.push(row);
269
+ } else {
270
+ numArr.push(row);
271
+ }
272
+ }
273
+ if (order === 'asc') {
274
+ numArr.sort((a, b) => +a[col.dataIndex] - +b[col.dataIndex]);
275
+ } else {
276
+ numArr.sort((a, b) => +b[col.dataIndex] - +a[col.dataIndex]);
277
+ }
278
+ this.dataSourceCopy = [...numArr, ...nanArr];
279
+ } else {
280
+ // 按string 排序
281
+ if (order === 'asc') {
282
+ this.dataSourceCopy.sort((a, b) => (a[col.dataIndex] < b[col.dataIndex] ? -1 : 1));
283
+ } else {
284
+ this.dataSourceCopy.sort((a, b) => (a[col.dataIndex] > b[col.dataIndex] ? -1 : 1));
285
+ }
286
+ }
287
+ } else {
288
+ this.dataSourceCopy = [...this.dataSource];
289
+ }
290
+ },
291
+ onRowClick(row) {
292
+ this.currentItem = row;
293
+ this.$emit('current-change', row);
294
+ },
295
+ onRowDblclick(row) {
296
+ this.$emit('row-dblclick', row);
297
+ },
298
+ onTableScroll(e) {
299
+ this.$refs.fixedLeftTable[0].style.left = e.target.scrollLeft + 'px';
300
+ },
301
+ // ---- ref function-----
302
+ /**
303
+ * 选中一行,
304
+ * @param {string} rowKey
305
+ * @param {boolean} option.silent 是否触发回调
306
+ */
307
+ setCurrentRow(rowKey, option = { silent: false }) {
308
+ if (!this.dataSourceCopy.length) return;
309
+ this.currentItem = this.dataSourceCopy.find(it => it[this.rowKey] === rowKey);
310
+ if (!option.silent) {
311
+ this.$emit('current-change', this.currentItem);
312
+ }
313
+ },
314
+ /** 高亮一个单元格 */
315
+ setHighlightDimCell(rowKeyValue, dataIndex) {
316
+ const cellEls = this.$el.querySelectorAll(`[data-row-key="${rowKeyValue}"]>[data-index="${dataIndex}"]`);
317
+ if (!cellEls?.length) return;
318
+ cellEls.forEach(cellEl => {
319
+ cellEl.classList.remove('highlight-cell');
320
+ void cellEl.offsetHeight;
321
+ cellEl.classList.add('highlight-cell');
322
+ });
323
+ },
324
+ /** 高亮一行 */
325
+ setHighlightDimRow(rowKeyValue) {
326
+ // 固定列的表格也要高亮
327
+ const rowEls = this.$el.querySelectorAll(`[data-row-key="${rowKeyValue}"]`);
328
+ if (!rowEls?.length) return;
329
+ rowEls.forEach(rowEl => {
330
+ rowEl.classList.remove('highlight-row');
331
+ void rowEl.offsetWidth;
332
+ rowEl.classList.add('highlight-row');
333
+ // 动画结束移除class
334
+ window.clearTimeout(this.highlightDimRowsTimeout.get(rowKeyValue));
335
+ this.highlightDimRowsTimeout.set(
336
+ rowKeyValue,
337
+ window.setTimeout(() => {
338
+ rowEl.classList.remove('highlight-row');
339
+ this.highlightDimRowsTimeout.delete(rowKeyValue); // 回收内存
340
+ }, 2000),
341
+ );
342
+ });
343
+ },
344
+ /**
345
+ * 设置排序
346
+ * @param {string} dataIndex
347
+ * @param {'asc'|'desc'|null} order
348
+ */
349
+ setSorter(dataIndex, order) {
350
+ this.sortCol = dataIndex;
351
+ this.sortOrderIndex = this.sortSwitchOrder.findIndex(it => it == order);
352
+ if (this.dataSourceCopy?.length) {
353
+ // 如果表格有数据,则进行排序
354
+ const column = this.columns.find(it => it.dataIndex === this.sortCol);
355
+ this.onColumnSort(column, false);
356
+ }
357
+ },
358
+ /** 重置排序 */
359
+ resetSorter() {
360
+ this.sortCol = null;
361
+ this.sortOrderIndex = 0;
362
+ this.dataSourceCopy = [...this.dataSource];
363
+ },
364
+ },
365
+ };
366
+ </script>
367
+
368
+ <style lang="less" scoped>
369
+ .stk-table-wrapper {
370
+ --row-height: 30px;
371
+ --border-color: #e8eaec;
372
+ // --border: 1px #ececf7 solid;
373
+ --td-bg-color: #fff;
374
+ --th-bg-color: #f8f8f9;
375
+ --td-padding: 8px;
376
+ --tr-active-bg-color: rgb(230, 247, 255);
377
+ --bg-border-top: linear-gradient(180deg, var(--border-color) 1px, transparent 1px);
378
+ --bg-border-right: linear-gradient(270deg, var(--border-color) 1px, transparent 1px);
379
+ --bg-border-bottom: linear-gradient(0deg, var(--border-color) 1px, transparent 1px);
380
+ --bg-border-left: linear-gradient(90deg, var(--border-color) 1px, transparent 1px);
381
+ --highlight-color: rgba(113, 162, 253, 1);
382
+ // --highlight-color-to: rgba(113, 162, 253, 0);
383
+ position: relative;
384
+ overflow: auto;
385
+ display: flex;
386
+ flex-direction: column;
387
+ .stk-table {
388
+ border-spacing: 0;
389
+ table-layout: fixed;
390
+ th,
391
+ td {
392
+ height: var(--row-height);
393
+ font-size: 14px;
394
+ box-sizing: border-box;
395
+ padding: 2px 5px;
396
+ padding: 0 var(--td-padding);
397
+ background-image: var(--bg-border-right), var(--bg-border-bottom);
398
+ }
399
+ thead {
400
+ tr {
401
+ &:first-child th {
402
+ position: sticky;
403
+ top: 0;
404
+ // border-top: 1px solid var(--border-color);
405
+ background-image: var(--bg-border-top), var(--bg-border-right), var(--bg-border-bottom);
406
+ &:first-child {
407
+ background-image: var(--bg-border-top), var(--bg-border-right), var(--bg-border-bottom),
408
+ var(--bg-border-left);
409
+ }
410
+ }
411
+ th {
412
+ background-color: var(--th-bg-color);
413
+ &.sortable {
414
+ cursor: pointer;
415
+ }
416
+ &:first-child {
417
+ // border-left: 1px solid var(--border-color);
418
+ background-image: var(--bg-border-top), var(--bg-border-right), var(--bg-border-bottom),
419
+ var(--bg-border-left);
420
+ padding-left: 12px;
421
+ }
422
+ &:last-child {
423
+ padding-right: 12px;
424
+ }
425
+ .table-header-cell-wrapper {
426
+ display: inline-flex;
427
+ align-items: center;
428
+ .table-header-title {
429
+ }
430
+ .table-header-sorter {
431
+ margin-left: 4px;
432
+ width: 16px;
433
+ height: 16px;
434
+ &:not(.sorter-desc):not(.sorter-asc):hover {
435
+ #arrow-up {
436
+ fill: #8f90b5;
437
+ }
438
+ #arrow-down {
439
+ fill: #8f90b5;
440
+ }
441
+ }
442
+ &.sorter-desc {
443
+ #arrow-up {
444
+ fill: #cbcbe1;
445
+ }
446
+ #arrow-down {
447
+ fill: #1b63d9;
448
+ }
449
+ }
450
+ &.sorter-asc {
451
+ #arrow-up {
452
+ fill: #1b63d9;
453
+ }
454
+ #arrow-down {
455
+ fill: #cbcbe1;
456
+ }
457
+ }
458
+ }
459
+ }
460
+ }
461
+ }
462
+ }
463
+ tbody {
464
+ /**高亮渐暗 */
465
+ @keyframes dim {
466
+ from {
467
+ background-color: var(--highlight-color);
468
+ }
469
+ }
470
+ tr {
471
+ &.highlight-row td:not(.highlight-cell) {
472
+ animation: dim 2s linear;
473
+ }
474
+ &.active {
475
+ td {
476
+ background-color: var(--tr-active-bg-color);
477
+ }
478
+ }
479
+ td {
480
+ background-color: var(--td-bg-color);
481
+ &:first-child {
482
+ // border-left: 1px solid var(--border-color);
483
+ background-image: var(--bg-border-right), var(--bg-border-bottom), var(--bg-border-left);
484
+ padding-left: 12px;
485
+ }
486
+ &:last-child {
487
+ padding-right: 12px;
488
+ }
489
+
490
+ &.highlight-cell {
491
+ animation: dim 2s linear;
492
+ }
493
+ }
494
+ }
495
+ // 斑马纹
496
+ // tr:nth-child(2n) td {
497
+ // background-color: #fafafc;
498
+ // }
499
+ // tr:hover {
500
+ // background-color: #ebf3ff;
501
+ // }
502
+ }
503
+ }
504
+ .stk-table.fixed-left {
505
+ position: absolute;
506
+ left: 0;
507
+ top: 0;
508
+ thead tr th:last-child {
509
+ padding-right: var(--td-padding);
510
+ }
511
+ tbody tr td:last-child {
512
+ padding-right: var(--td-padding);
513
+ }
514
+ }
515
+ .stk-table-no-data {
516
+ line-height: var(--row-height);
517
+ text-align: center;
518
+ font-size: 14px;
519
+ position: sticky;
520
+ width: 100%;
521
+ left: 0px;
522
+ background: var(--bg-border-left), var(--bg-border-bottom), var(--bg-border-right);
523
+ display: flex;
524
+ flex-direction: column;
525
+ align-items: center;
526
+ justify-content: center;
527
+ &.no-data-full {
528
+ flex: 1;
529
+ }
530
+ }
531
+
532
+ /**深色模式 */
533
+ &.dark {
534
+ // --th-bg-color: #26272c;
535
+ --th-bg-color: #181c21;
536
+ --td-bg-color: #181c21;
537
+ --border-color: #2e2e33;
538
+ --tr-active-bg-color: #1a2b46;
539
+ --highlight-color: rgba(19, 55, 125, 1);
540
+ // --highlight-color-to: rgba(19, 55, 125, 0);
541
+ background-color: var(--th-bg-color);
542
+ color: #d0d1d2;
543
+ .stk-table {
544
+ thead {
545
+ tr {
546
+ th {
547
+ .table-header-cell-wrapper {
548
+ .table-header-sorter {
549
+ #arrow-up,
550
+ #arrow-down {
551
+ fill: #5d5f69;
552
+ }
553
+ &:not(.sorter-desc):not(.sorter-asc):hover {
554
+ #arrow-up {
555
+ fill: #727782;
556
+ }
557
+ #arrow-down {
558
+ fill: #727782;
559
+ }
560
+ }
561
+ &.sorter-desc {
562
+ #arrow-up {
563
+ fill: #5d5f69;
564
+ }
565
+ #arrow-down {
566
+ fill: #4f8df4;
567
+ }
568
+ }
569
+ &.sorter-asc {
570
+ #arrow-up {
571
+ fill: #4f8df4;
572
+ }
573
+ #arrow-down {
574
+ fill: #5d5f69;
575
+ }
576
+ }
577
+ }
578
+ }
579
+ }
580
+ }
581
+ }
582
+ }
583
+ tbody {
584
+ tr.row-hover td {
585
+ box-shadow: 0px -1px 0 #1b63d9 inset;
586
+ }
587
+ }
588
+ }
589
+ }
590
+ </style>