@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.
@@ -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
- tr
10
- template(v-for="field, index in fields" :key="field.key")
11
- slot(
12
- v-if="$slots[`${field.key}-header`]"
13
- :name="`${field.key}-header`"
14
- :field="field"
15
- :index="index"
16
- )
17
- th(
18
- v-else
19
- :style="field._style"
20
- :class="[field._class, { sortable: field.sortable, sorting: field.key === sortBy, 'sort-desc': sortDesc}]"
21
- :colspan="headerColumnSpans[index]"
22
- :key="field.key"
23
- )
24
- .inline-flex.items-center.space-x-1
25
- SLElementWithTitle(v-if='field.info', :title="field.info")
26
- SLIcon.text-primary(icon='info-circle')
27
- component(:is='field.sortable ? "a" : "div"', href='javascript:;' class='inline-flex items-center', @click="field.sortable && sort(field.key)")
28
- span {{ field.label }}
29
- SLIcon.sort-icon.ml-2(v-if="field.sortable" :icon="sortDesc ? 'descending' : 'ascending'")
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 : '&nbsp;' }}
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
- .my-28.text-base.text-gray-2.text-center {{ noItems }}
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
- v-if="loading"
78
- :boundaries="[{ sides: ['top'], query: 'td' }, { sides: ['bottom'], query: 'tbody' }]"
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 pointer
403
+ &.sortable, &.filterable
404
+ @apply cursor-pointer
230
405
  .sort-icon
231
- opacity 0
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 1
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
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mptw/skylens-ui",
3
3
  "type": "module",
4
- "version": "1.3.0",
4
+ "version": "1.3.2",
5
5
  "main": "./nuxt.config.ts",
6
6
  "scripts": {
7
7
  "dev": "nuxi dev .playground",