@mptw/skylens-ui 1.3.0 → 1.3.2
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.
- package/components/SLTable.vue +230 -30
- package/package.json +1 -1
package/components/SLTable.vue
CHANGED
|
@@ -1,32 +1,47 @@
|
|
|
1
1
|
<template lang="pug">
|
|
2
|
-
div
|
|
2
|
+
div(:class='componentClass', ref="el")
|
|
3
3
|
.table(
|
|
4
4
|
:class="{ 'table-responsive': responsive }"
|
|
5
5
|
@mousemove="onMousemoved"
|
|
6
6
|
)
|
|
7
7
|
table
|
|
8
8
|
thead
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
9
|
+
slot(name="thead")
|
|
10
|
+
tr
|
|
11
|
+
template(v-for="field, index in fields" :key="field.key")
|
|
12
|
+
slot(
|
|
13
|
+
v-if="$slots[`${field.key}-header`]"
|
|
14
|
+
:name="`${field.key}-header`"
|
|
15
|
+
:field="field"
|
|
16
|
+
:index="index"
|
|
17
|
+
)
|
|
18
|
+
th(
|
|
19
|
+
v-else
|
|
20
|
+
:style="field._style",
|
|
21
|
+
:class="[field._class, { sortable: field.sortable, sorting: field.key === sortBy, 'sort-desc': sortDesc, filterable: field.filterable, filtering: filterBy !== ''}]",
|
|
22
|
+
:colspan="headerColumnSpans[index]",
|
|
23
|
+
:name="`${field.key}-header`",
|
|
24
|
+
:key="field.key",
|
|
25
|
+
@click.prevent="onClickedHeaderCol(field.key)"
|
|
26
|
+
)
|
|
27
|
+
.inline-flex.items-center.space-x-1
|
|
28
|
+
SLElementWithTitle(v-if='field.info', :title="field.info")
|
|
29
|
+
SLIcon.text-primary(icon='info-circle')
|
|
30
|
+
SLElementWithTitle(:title="field.tooltip")
|
|
31
|
+
span {{ field.label ? field.label : ' ' }}
|
|
32
|
+
SLIcon.sort-icon(
|
|
33
|
+
v-if="field.sortable"
|
|
34
|
+
class="ml-0.5"
|
|
35
|
+
:icon="sortDesc ? 'descending' : 'ascending'"
|
|
36
|
+
)
|
|
37
|
+
//- component(:is='field.sortable ? "a" : "div"', href='javascript:;' class='inline-flex items-center', @click="field.sortable && sort(field.key)")
|
|
38
|
+
//- span {{ field.label }}
|
|
39
|
+
//- SLIcon.sort-icon.ml-2(v-if="field.sortable" :icon="sortDesc ? 'descending' : 'ascending'")
|
|
40
|
+
SLIcon.filter-icon(
|
|
41
|
+
v-if="field.filterable"
|
|
42
|
+
class="ml-0.5"
|
|
43
|
+
:icon="filterBy !== '' ? 'filter-on' : 'filter'"
|
|
44
|
+
)
|
|
30
45
|
tbody(:class="{ 'hoverable': hoverableRows, 'clickable': clickableRows }")
|
|
31
46
|
template(v-for="(item, itemIndex) in bodyHeaderItems" :key="itemIndex")
|
|
32
47
|
tr.body-header-row(
|
|
@@ -72,15 +87,38 @@
|
|
|
72
87
|
) {{ item[field.key] }}
|
|
73
88
|
tr(v-if="items.length === 0")
|
|
74
89
|
td(:colspan="fields.length")
|
|
75
|
-
|
|
90
|
+
div(v-if="loading", style="min-height: 200px")
|
|
91
|
+
.flex.flex-col.items-center.py-8.px-4.space-y-3(v-else)
|
|
92
|
+
img(src="~/assets/images/commons/empty-box.png")
|
|
93
|
+
.text-lg.text-primary-light {{ noItems }}
|
|
94
|
+
.text-sm.text-gray-3(v-if="reason") 可能的原因:{{ reason }}
|
|
76
95
|
SLLoading(
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
96
|
+
v-if="loading"
|
|
97
|
+
:boundaries="[{ sides: ['top'], query: 'td' }, { sides: ['bottom'], query: 'tbody' }]"
|
|
98
|
+
)
|
|
99
|
+
.table-header-filter-popup(ref="popupEl")
|
|
100
|
+
.table-header-filter-popup-content
|
|
101
|
+
a.table-header-filter-item(
|
|
102
|
+
v-for="option in filterOptions"
|
|
103
|
+
href="javascript:;"
|
|
104
|
+
:class="{ active: filterBy === option.key }"
|
|
105
|
+
:key="option.key"
|
|
106
|
+
@click="onClickedFilter(option.key)"
|
|
107
|
+
)
|
|
108
|
+
.inline-flex.items-center.space-x-1
|
|
109
|
+
SLIcon(
|
|
110
|
+
v-if="option.icon"
|
|
111
|
+
:icon="option.icon"
|
|
112
|
+
:class="option._iconClass"
|
|
113
|
+
)
|
|
114
|
+
span(v-if="option.label") {{ option.label }}
|
|
80
115
|
</template>
|
|
81
116
|
|
|
82
117
|
<script lang="ts">
|
|
83
118
|
import type { PropType } from 'vue';
|
|
119
|
+
import { fromEvent, Subscription } from 'rxjs';
|
|
120
|
+
import { createPopper } from '@popperjs/core';
|
|
121
|
+
import type { Instance as PopperInstance } from '@popperjs/core';
|
|
84
122
|
import type ITableField from '@/interface/ITableField';
|
|
85
123
|
|
|
86
124
|
export default defineComponent({
|
|
@@ -88,6 +126,10 @@
|
|
|
88
126
|
components: {
|
|
89
127
|
},
|
|
90
128
|
props: {
|
|
129
|
+
componentClass: {
|
|
130
|
+
type: [String, Array, Object],
|
|
131
|
+
default: '',
|
|
132
|
+
},
|
|
91
133
|
items: {
|
|
92
134
|
type: Array,
|
|
93
135
|
default: () => [],
|
|
@@ -108,6 +150,10 @@
|
|
|
108
150
|
type: Boolean,
|
|
109
151
|
default: false,
|
|
110
152
|
},
|
|
153
|
+
filterBy: {
|
|
154
|
+
type: String,
|
|
155
|
+
default: '',
|
|
156
|
+
},
|
|
111
157
|
hoverableRows: {
|
|
112
158
|
type: Boolean,
|
|
113
159
|
default: false,
|
|
@@ -136,12 +182,38 @@
|
|
|
136
182
|
type: String,
|
|
137
183
|
default: 'No Items',
|
|
138
184
|
},
|
|
185
|
+
reason: {
|
|
186
|
+
type: String,
|
|
187
|
+
default: '',
|
|
188
|
+
},
|
|
139
189
|
},
|
|
190
|
+
emits: [
|
|
191
|
+
'update:sortDesc',
|
|
192
|
+
'update:sortBy',
|
|
193
|
+
'update:filterBy',
|
|
194
|
+
'row-clicked',
|
|
195
|
+
'row-mouseentered',
|
|
196
|
+
'row-mouseleaved',
|
|
197
|
+
'table-mousemoved',
|
|
198
|
+
],
|
|
140
199
|
setup(props, { emit }) {
|
|
141
200
|
const { fields, sortBy, sortDesc, clickableRows, hoverableRows } =
|
|
142
201
|
toRefs(props);
|
|
143
202
|
|
|
203
|
+
const el = shallowRef<HTMLElement | null>(null);
|
|
204
|
+
const popupEl = shallowRef<HTMLElement | null>(null);
|
|
144
205
|
const hoverRowIndex = ref(-1);
|
|
206
|
+
const filterOptions = ref<
|
|
207
|
+
{
|
|
208
|
+
key: string;
|
|
209
|
+
label?: string;
|
|
210
|
+
icon?: string;
|
|
211
|
+
}[]
|
|
212
|
+
>([]);
|
|
213
|
+
|
|
214
|
+
let popperInstance: PopperInstance | null = null;
|
|
215
|
+
let clickSubscriber: Subscription | null = null;
|
|
216
|
+
let filterHeaderCol: HTMLElement | null = null;
|
|
145
217
|
|
|
146
218
|
const headerColumnSpans = computed(() => {
|
|
147
219
|
return fields.value.map((field) => {
|
|
@@ -164,6 +236,22 @@
|
|
|
164
236
|
emit('update:sortBy', fieldKey);
|
|
165
237
|
}
|
|
166
238
|
}
|
|
239
|
+
|
|
240
|
+
function onClickedHeaderCol(fieldKey: string) {
|
|
241
|
+
const field = fields.value.find((f) => f.key === fieldKey);
|
|
242
|
+
if (!field || (!field.sortable && !field.filterable)) return;
|
|
243
|
+
|
|
244
|
+
if (field.sortable) {
|
|
245
|
+
sort(field.key);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (field.filterable) {
|
|
249
|
+
filterOptions.value = [...field.filterOptions];
|
|
250
|
+
filterHeaderCol = el.value.querySelector(`th[name=${field.key}-header]`)
|
|
251
|
+
.children[0] as HTMLElement;
|
|
252
|
+
show();
|
|
253
|
+
}
|
|
254
|
+
}
|
|
167
255
|
|
|
168
256
|
function rowClicked(item: any, index: number, e: Event) {
|
|
169
257
|
if (!clickableRows.value) return;
|
|
@@ -190,16 +278,103 @@
|
|
|
190
278
|
|
|
191
279
|
emit('table-mousemoved', e);
|
|
192
280
|
}
|
|
281
|
+
|
|
282
|
+
function subscribeClick() {
|
|
283
|
+
clickSubscriber = fromEvent(window, 'click').subscribe((e) => {
|
|
284
|
+
const { target } = e;
|
|
285
|
+
if (
|
|
286
|
+
filterHeaderCol &&
|
|
287
|
+
popupEl.value &&
|
|
288
|
+
!filterHeaderCol.contains(target as HTMLElement) &&
|
|
289
|
+
!popupEl.value.contains(target as HTMLElement)
|
|
290
|
+
) {
|
|
291
|
+
hide();
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function unsubscribeClick() {
|
|
297
|
+
if (clickSubscriber) {
|
|
298
|
+
clickSubscriber.unsubscribe();
|
|
299
|
+
}
|
|
300
|
+
clickSubscriber = null;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function show() {
|
|
304
|
+
if (!popupEl.value || !filterHeaderCol || !popperInstance) return;
|
|
305
|
+
|
|
306
|
+
popupEl.value.setAttribute('data-show', '');
|
|
307
|
+
|
|
308
|
+
subscribeClick();
|
|
309
|
+
popperInstance.state.elements.reference = filterHeaderCol;
|
|
310
|
+
popperInstance.setOptions({
|
|
311
|
+
modifiers: [
|
|
312
|
+
{
|
|
313
|
+
name: 'offset',
|
|
314
|
+
options: {
|
|
315
|
+
offset: [0, 4],
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
{ name: 'eventListeners', enabled: true },
|
|
319
|
+
],
|
|
320
|
+
});
|
|
321
|
+
popperInstance.update();
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function hide() {
|
|
325
|
+
if (
|
|
326
|
+
!popupEl.value ||
|
|
327
|
+
popupEl.value.getAttribute('data-show') === null ||
|
|
328
|
+
!popperInstance
|
|
329
|
+
)
|
|
330
|
+
return;
|
|
331
|
+
|
|
332
|
+
unsubscribeClick();
|
|
333
|
+
|
|
334
|
+
if (popupEl.value) {
|
|
335
|
+
popupEl.value.removeAttribute('data-show');
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
popperInstance.setOptions({
|
|
339
|
+
modifiers: [{ name: 'eventListeners', enabled: false }],
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function onClickedFilter(filterKey: string) {
|
|
344
|
+
emit('update:filterBy', filterKey);
|
|
345
|
+
hide();
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
onMounted(() => {
|
|
349
|
+
if (el.value && popupEl.value) {
|
|
350
|
+
popperInstance = createPopper(el.value, popupEl.value, {
|
|
351
|
+
placement: 'bottom-end',
|
|
352
|
+
modifiers: [{ name: 'eventListeners', enabled: false }],
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
onBeforeUnmount(() => {
|
|
358
|
+
if (popperInstance) {
|
|
359
|
+
popperInstance.destroy();
|
|
360
|
+
}
|
|
361
|
+
popperInstance = null;
|
|
362
|
+
});
|
|
193
363
|
|
|
194
364
|
return {
|
|
365
|
+
el,
|
|
366
|
+
popupEl,
|
|
195
367
|
hoverRowIndex,
|
|
368
|
+
filterOptions,
|
|
196
369
|
headerColumnSpans,
|
|
197
370
|
columnSpans,
|
|
371
|
+
onClickedHeaderCol,
|
|
198
372
|
sort,
|
|
199
373
|
rowClicked,
|
|
200
374
|
rowMouseEntered,
|
|
201
375
|
rowMouseLeaved,
|
|
202
376
|
onMousemoved,
|
|
377
|
+
onClickedFilter,
|
|
203
378
|
};
|
|
204
379
|
},
|
|
205
380
|
});
|
|
@@ -225,15 +400,18 @@
|
|
|
225
400
|
th
|
|
226
401
|
@apply px-2 pb-2 text-base font-normal text-primary whitespace-nowrap
|
|
227
402
|
|
|
228
|
-
&.sortable
|
|
229
|
-
cursor
|
|
403
|
+
&.sortable, &.filterable
|
|
404
|
+
@apply cursor-pointer
|
|
230
405
|
.sort-icon
|
|
231
|
-
opacity
|
|
406
|
+
@apply opacity-0
|
|
232
407
|
transition transform 0.2s ease, opacity 0.1s ease
|
|
233
408
|
&:hover,
|
|
234
409
|
&.sorting
|
|
235
410
|
.sort-icon
|
|
236
|
-
opacity
|
|
411
|
+
@apply opacity-100
|
|
412
|
+
&.filterable
|
|
413
|
+
.filter-icon
|
|
414
|
+
@apply opacity-100
|
|
237
415
|
|
|
238
416
|
tbody
|
|
239
417
|
&.clickable
|
|
@@ -250,5 +428,27 @@
|
|
|
250
428
|
|
|
251
429
|
&.text-right.sortable
|
|
252
430
|
@apply pr-8
|
|
431
|
+
|
|
432
|
+
.table-header-filter-popup
|
|
433
|
+
@apply z-10 flex border border-primary-light rounded overflow-hidden invisible pointer-events-none
|
|
434
|
+
|
|
435
|
+
&[data-show]
|
|
436
|
+
@apply visible pointer-events-auto
|
|
437
|
+
|
|
438
|
+
&-content
|
|
439
|
+
@apply flex flex-col
|
|
440
|
+
|
|
441
|
+
.table-header-filter-item
|
|
442
|
+
@apply inline-flex justify-center p-2 bg-white text-sm text-gray-3
|
|
443
|
+
@apply s('hover:bg-bg-middle')
|
|
444
|
+
|
|
445
|
+
&.active
|
|
446
|
+
@apply bg-bg text-primary
|
|
447
|
+
|
|
448
|
+
.icon
|
|
449
|
+
@apply text-primary
|
|
450
|
+
|
|
451
|
+
.icon
|
|
452
|
+
@apply text-base leading-none
|
|
253
453
|
</style>
|
|
254
454
|
|