@mptw/skylens-ui 1.3.0 → 1.3.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.
@@ -1,5 +1,5 @@
1
1
  <template lang="pug">
2
- div
2
+ div(ref="el")
3
3
  .table(
4
4
  :class="{ 'table-responsive': responsive }"
5
5
  @mousemove="onMousemoved"
@@ -16,10 +16,12 @@
16
16
  )
17
17
  th(
18
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"
19
+ :style="field._style",
20
+ :class="[field._class, { sortable: field.sortable, sorting: field.key === sortBy, 'sort-desc': sortDesc, filterable: field.filterable, filtering: filterBy !== ''}]",
21
+ :colspan="headerColumnSpans[index]",
22
+ :name="`${field.key}-header`",
23
+ :key="field.key",
24
+ @click.prevent="onClickedHeaderCol(field.key)"
23
25
  )
24
26
  .inline-flex.items-center.space-x-1
25
27
  SLElementWithTitle(v-if='field.info', :title="field.info")
@@ -27,6 +29,12 @@
27
29
  component(:is='field.sortable ? "a" : "div"', href='javascript:;' class='inline-flex items-center', @click="field.sortable && sort(field.key)")
28
30
  span {{ field.label }}
29
31
  SLIcon.sort-icon.ml-2(v-if="field.sortable" :icon="sortDesc ? 'descending' : 'ascending'")
32
+
33
+ SLIcon.filter-icon(
34
+ v-if="field.filterable"
35
+ class="ml-0.5"
36
+ :icon="filterBy !== '' ? 'filter-on' : 'filter'"
37
+ )
30
38
  tbody(:class="{ 'hoverable': hoverableRows, 'clickable': clickableRows }")
31
39
  template(v-for="(item, itemIndex) in bodyHeaderItems" :key="itemIndex")
32
40
  tr.body-header-row(
@@ -72,15 +80,38 @@
72
80
  ) {{ item[field.key] }}
73
81
  tr(v-if="items.length === 0")
74
82
  td(:colspan="fields.length")
75
- .my-28.text-base.text-gray-2.text-center {{ noItems }}
83
+ div(v-if="loading", style="min-height: 200px")
84
+ .flex.flex-col.items-center.py-8.px-4.space-y-3(v-else)
85
+ img(src="~/assets/images/commons/empty-box.png")
86
+ .text-lg.text-primary-light {{ noItems }}
87
+ .text-sm.text-gray-3(v-if="reason") 可能的原因:{{ reason }}
76
88
  SLLoading(
77
- v-if="loading"
78
- :boundaries="[{ sides: ['top'], query: 'td' }, { sides: ['bottom'], query: 'tbody' }]"
79
- )
89
+ v-if="loading"
90
+ :boundaries="[{ sides: ['top'], query: 'td' }, { sides: ['bottom'], query: 'tbody' }]"
91
+ )
92
+ .table-header-filter-popup(ref="popupEl")
93
+ .table-header-filter-popup-content
94
+ a.table-header-filter-item(
95
+ v-for="option in filterOptions"
96
+ href="javascript:;"
97
+ :class="{ active: filterBy === option.key }"
98
+ :key="option.key"
99
+ @click="onClickedFilter(option.key)"
100
+ )
101
+ .inline-flex.items-center.space-x-1
102
+ SLIcon(
103
+ v-if="option.icon"
104
+ :icon="option.icon"
105
+ :class="option._iconClass"
106
+ )
107
+ span(v-if="option.label") {{ option.label }}
80
108
  </template>
81
109
 
82
110
  <script lang="ts">
83
111
  import type { PropType } from 'vue';
112
+ import { fromEvent, Subscription } from 'rxjs';
113
+ import { createPopper } from '@popperjs/core';
114
+ import type { Instance as PopperInstance } from '@popperjs/core';
84
115
  import type ITableField from '@/interface/ITableField';
85
116
 
86
117
  export default defineComponent({
@@ -108,6 +139,10 @@
108
139
  type: Boolean,
109
140
  default: false,
110
141
  },
142
+ filterBy: {
143
+ type: String,
144
+ default: '',
145
+ },
111
146
  hoverableRows: {
112
147
  type: Boolean,
113
148
  default: false,
@@ -136,12 +171,38 @@
136
171
  type: String,
137
172
  default: 'No Items',
138
173
  },
174
+ reason: {
175
+ type: String,
176
+ default: '',
177
+ },
139
178
  },
179
+ emits: [
180
+ 'update:sortDesc',
181
+ 'update:sortBy',
182
+ 'update:filterBy',
183
+ 'row-clicked',
184
+ 'row-mouseentered',
185
+ 'row-mouseleaved',
186
+ 'table-mousemoved',
187
+ ],
140
188
  setup(props, { emit }) {
141
189
  const { fields, sortBy, sortDesc, clickableRows, hoverableRows } =
142
190
  toRefs(props);
143
191
 
192
+ const el = shallowRef<HTMLElement | null>(null);
193
+ const popupEl = shallowRef<HTMLElement | null>(null);
144
194
  const hoverRowIndex = ref(-1);
195
+ const filterOptions = ref<
196
+ {
197
+ key: string;
198
+ label?: string;
199
+ icon?: string;
200
+ }[]
201
+ >([]);
202
+
203
+ let popperInstance: PopperInstance | null = null;
204
+ let clickSubscriber: Subscription | null = null;
205
+ let filterHeaderCol: HTMLElement | null = null;
145
206
 
146
207
  const headerColumnSpans = computed(() => {
147
208
  return fields.value.map((field) => {
@@ -164,6 +225,22 @@
164
225
  emit('update:sortBy', fieldKey);
165
226
  }
166
227
  }
228
+
229
+ function onClickedHeaderCol(fieldKey: string) {
230
+ const field = fields.value.find((f) => f.key === fieldKey);
231
+ if (!field || (!field.sortable && !field.filterable)) return;
232
+
233
+ if (field.sortable) {
234
+ sort(field.key);
235
+ }
236
+
237
+ if (field.filterable) {
238
+ filterOptions.value = [...field.filterOptions];
239
+ filterHeaderCol = el.value.querySelector(`th[name=${field.key}-header]`)
240
+ .children[0] as HTMLElement;
241
+ show();
242
+ }
243
+ }
167
244
 
168
245
  function rowClicked(item: any, index: number, e: Event) {
169
246
  if (!clickableRows.value) return;
@@ -190,16 +267,103 @@
190
267
 
191
268
  emit('table-mousemoved', e);
192
269
  }
270
+
271
+ function subscribeClick() {
272
+ clickSubscriber = fromEvent(window, 'click').subscribe((e) => {
273
+ const { target } = e;
274
+ if (
275
+ filterHeaderCol &&
276
+ popupEl.value &&
277
+ !filterHeaderCol.contains(target as HTMLElement) &&
278
+ !popupEl.value.contains(target as HTMLElement)
279
+ ) {
280
+ hide();
281
+ }
282
+ });
283
+ }
284
+
285
+ function unsubscribeClick() {
286
+ if (clickSubscriber) {
287
+ clickSubscriber.unsubscribe();
288
+ }
289
+ clickSubscriber = null;
290
+ }
291
+
292
+ function show() {
293
+ if (!popupEl.value || !filterHeaderCol || !popperInstance) return;
294
+
295
+ popupEl.value.setAttribute('data-show', '');
296
+
297
+ subscribeClick();
298
+ popperInstance.state.elements.reference = filterHeaderCol;
299
+ popperInstance.setOptions({
300
+ modifiers: [
301
+ {
302
+ name: 'offset',
303
+ options: {
304
+ offset: [0, 4],
305
+ },
306
+ },
307
+ { name: 'eventListeners', enabled: true },
308
+ ],
309
+ });
310
+ popperInstance.update();
311
+ }
312
+
313
+ function hide() {
314
+ if (
315
+ !popupEl.value ||
316
+ popupEl.value.getAttribute('data-show') === null ||
317
+ !popperInstance
318
+ )
319
+ return;
320
+
321
+ unsubscribeClick();
322
+
323
+ if (popupEl.value) {
324
+ popupEl.value.removeAttribute('data-show');
325
+ }
326
+
327
+ popperInstance.setOptions({
328
+ modifiers: [{ name: 'eventListeners', enabled: false }],
329
+ });
330
+ }
331
+
332
+ function onClickedFilter(filterKey: string) {
333
+ emit('update:filterBy', filterKey);
334
+ hide();
335
+ }
336
+
337
+ onMounted(() => {
338
+ if (el.value && popupEl.value) {
339
+ popperInstance = createPopper(el.value, popupEl.value, {
340
+ placement: 'bottom-end',
341
+ modifiers: [{ name: 'eventListeners', enabled: false }],
342
+ });
343
+ }
344
+ });
345
+
346
+ onBeforeUnmount(() => {
347
+ if (popperInstance) {
348
+ popperInstance.destroy();
349
+ }
350
+ popperInstance = null;
351
+ });
193
352
 
194
353
  return {
354
+ el,
355
+ popupEl,
195
356
  hoverRowIndex,
357
+ filterOptions,
196
358
  headerColumnSpans,
197
359
  columnSpans,
360
+ onClickedHeaderCol,
198
361
  sort,
199
362
  rowClicked,
200
363
  rowMouseEntered,
201
364
  rowMouseLeaved,
202
365
  onMousemoved,
366
+ onClickedFilter,
203
367
  };
204
368
  },
205
369
  });
@@ -225,15 +389,18 @@
225
389
  th
226
390
  @apply px-2 pb-2 text-base font-normal text-primary whitespace-nowrap
227
391
 
228
- &.sortable
229
- cursor pointer
392
+ &.sortable, &.filterable
393
+ @apply cursor-pointer
230
394
  .sort-icon
231
- opacity 0
395
+ @apply opacity-0
232
396
  transition transform 0.2s ease, opacity 0.1s ease
233
397
  &:hover,
234
398
  &.sorting
235
399
  .sort-icon
236
- opacity 1
400
+ @apply opacity-100
401
+ &.filterable
402
+ .filter-icon
403
+ @apply opacity-100
237
404
 
238
405
  tbody
239
406
  &.clickable
@@ -250,5 +417,27 @@
250
417
 
251
418
  &.text-right.sortable
252
419
  @apply pr-8
420
+
421
+ .table-header-filter-popup
422
+ @apply z-10 flex border border-primary-light rounded overflow-hidden invisible pointer-events-none
423
+
424
+ &[data-show]
425
+ @apply visible pointer-events-auto
426
+
427
+ &-content
428
+ @apply flex flex-col
429
+
430
+ .table-header-filter-item
431
+ @apply inline-flex justify-center p-2 bg-white text-sm text-gray-3
432
+ @apply s('hover:bg-bg-middle')
433
+
434
+ &.active
435
+ @apply bg-bg text-primary
436
+
437
+ .icon
438
+ @apply text-primary
439
+
440
+ .icon
441
+ @apply text-base leading-none
253
442
  </style>
254
443
 
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.1",
5
5
  "main": "./nuxt.config.ts",
6
6
  "scripts": {
7
7
  "dev": "nuxi dev .playground",