adminforth 2.4.0-next.138 → 2.4.0-next.139
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/dist/spa/src/afcl/Table.vue +154 -176
- package/package.json +1 -1
|
@@ -1,130 +1,128 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
|
|
3
|
-
<div class="
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
>
|
|
43
|
-
<td class="px-6 py-4"
|
|
44
|
-
v-for="column in props.columns"
|
|
45
|
-
>
|
|
46
|
-
<slot v-if="$slots[`cell:${column.fieldName}`]"
|
|
47
|
-
:name="`cell:${column.fieldName}`"
|
|
48
|
-
:item="item" :column="column"
|
|
2
|
+
<div class="afcl-table-container relative overflow-x-auto shadow-md sm:rounded-lg">
|
|
3
|
+
<div class="overflow-x-auto w-full">
|
|
4
|
+
<table class="afcl-table w-full text-sm text-left rtl:text-right text-lightTableText dark:text-darkTableText overflow-x-auto">
|
|
5
|
+
<thead class="afcl-table-thread text-xs text-lightTableHeadingText uppercase bg-lightTableHeadingBackground dark:bg-darkTableHeadingBackground dark:text-darkTableHeadingText">
|
|
6
|
+
<tr>
|
|
7
|
+
<th scope="col" class="px-6 py-3" ref="headerRefs"
|
|
8
|
+
v-for="column in columns"
|
|
9
|
+
>
|
|
10
|
+
<slot v-if="$slots[`header:${column.fieldName}`]" :name="`header:${column.fieldName}`" :column="column" />
|
|
11
|
+
|
|
12
|
+
<span v-else>
|
|
13
|
+
{{ column.label }}
|
|
14
|
+
</span>
|
|
15
|
+
</th>
|
|
16
|
+
</tr>
|
|
17
|
+
</thead>
|
|
18
|
+
<tbody>
|
|
19
|
+
<SkeleteLoader
|
|
20
|
+
v-if="isLoading"
|
|
21
|
+
:rows="pageSize"
|
|
22
|
+
:columns="columns.length"
|
|
23
|
+
:row-heights="rowHeights"
|
|
24
|
+
:column-widths="columnWidths"
|
|
25
|
+
/>
|
|
26
|
+
<tr v-else-if="!isLoading && dataPage.length === 0" class="afcl-table-empty-body">
|
|
27
|
+
<td :colspan="columns.length" class="px-6 py-12 text-center">
|
|
28
|
+
<div class="flex flex-col items-center justify-center text-lightTableText dark:text-darkTableText">
|
|
29
|
+
<IconTableRowOutline class="w-10 h-10 mb-4 text-gray-400 dark:text-gray-500" />
|
|
30
|
+
<p class="text-md">{{ $t('No data available') }}</p>
|
|
31
|
+
</div>
|
|
32
|
+
</td>
|
|
33
|
+
</tr>
|
|
34
|
+
<tr
|
|
35
|
+
v-else="!isLoading"
|
|
36
|
+
v-for="(item, index) in dataPage"
|
|
37
|
+
ref="rowRefs"
|
|
38
|
+
:class="{
|
|
39
|
+
'afcl-table-body odd:bg-lightTableOddBackground odd:dark:bg-darkTableOddBackground even:bg-lightTableEvenBackground even:dark:bg-darkTableEvenBackground': evenHighlights,
|
|
40
|
+
'border-b border-lightTableBorder dark:border-darkTableBorder': index !== dataPage.length - 1 || totalPages > 1,
|
|
41
|
+
}"
|
|
49
42
|
>
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
<
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
<
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
43
|
+
<td class="px-6 py-4"
|
|
44
|
+
v-for="column in props.columns"
|
|
45
|
+
>
|
|
46
|
+
<slot v-if="$slots[`cell:${column.fieldName}`]"
|
|
47
|
+
:name="`cell:${column.fieldName}`"
|
|
48
|
+
:item="item" :column="column"
|
|
49
|
+
>
|
|
50
|
+
</slot>
|
|
51
|
+
<span v-else-if="!isLoading" >
|
|
52
|
+
{{ item[column.fieldName] }}
|
|
53
|
+
</span>
|
|
54
|
+
<div v-else>
|
|
55
|
+
<div class=" w-full">
|
|
56
|
+
<Skeleton class="h-4" />
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
</td>
|
|
60
|
+
</tr>
|
|
61
|
+
</tbody>
|
|
62
|
+
</table>
|
|
63
|
+
</div>
|
|
64
|
+
<nav class="afcl-table-pagination-container bg-lightTableBackground dark:bg-darkTableBackground mt-2 flex flex-col gap-2 items-center sm:flex-row justify-center sm:justify-between px-4 pb-4"
|
|
65
|
+
v-if="totalPages > 1"
|
|
66
|
+
:aria-label="$t('Table navigation')">
|
|
67
|
+
<i18n-t
|
|
68
|
+
keypath="Showing {from} to {to} of {total}" tag="span" class="afcl-table-pagination-text text-sm font-normal text-center text-lightTablePaginationText dark:text-darkTablePaginationText sm:mb-4 md:mb-0 block w-full md:inline md:w-auto"
|
|
69
|
+
>
|
|
70
|
+
<template #from><span class="font-semibold text-lightTablePaginationNumeration dark:text-darkTablePaginationNumeration">{{ Math.min((currentPage - 1) * props.pageSize + 1, dataResult.total) }}</span></template>
|
|
71
|
+
<template #to><span class="font-semibold text-lightTablePaginationNumeration dark:text-darkTablePaginationNumeration">{{ Math.min(currentPage * props.pageSize, dataResult.total) }}</span></template>
|
|
72
|
+
<template #total><span class="font-semibold text-lightTablePaginationNumeration dark:text-darkTablePaginationNumeration">{{ dataResult.total }}</span></template>
|
|
73
|
+
</i18n-t>
|
|
74
|
+
<div class="af-pagination-container flex flex-row items-center xs:flex-row xs:justify-between xs:items-center gap-3">
|
|
75
|
+
<div class="inline-flex">
|
|
76
|
+
<!-- Buttons -->
|
|
77
|
+
<button
|
|
78
|
+
class="flex items-center py-1 px-3 gap-1 text-sm font-medium text-lightActivePaginationButtonText bg-lightActivePaginationButtonBackground border-r-0 rounded-s hover:opacity-90 dark:bg-darkActivePaginationButtonBackground dark:text-darkActivePaginationButtonText disabled:opacity-50"
|
|
79
|
+
@click="currentPage--; pageInput = currentPage.toString();" :disabled="currentPage <= 1">
|
|
80
|
+
<svg class="w-3.5 h-3.5 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none"
|
|
81
|
+
viewBox="0 0 14 10">
|
|
82
|
+
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
83
|
+
d="M13 5H1m0 0 4 4M1 5l4-4"/>
|
|
84
|
+
</svg>
|
|
85
|
+
</button>
|
|
86
|
+
<button
|
|
87
|
+
class="flex items-center py-1 px-3 text-sm font-medium text-lightUnactivePaginationButtonText focus:outline-none bg-lightUnactivePaginationButtonBackground border-r-0 border border-lightUnactivePaginationButtonBorder hover:bg-lightUnactivePaginationButtonHoverBackground hover:text-lightUnactivePaginationButtonHoverText dark:bg-darkUnactivePaginationButtonBackground dark:text-darkUnactivePaginationButtonText dark:border-darkUnactivePaginationButtonBorder dark:hover:text-darkUnactivePaginationButtonHoverText dark:hover:bg-darkUnactivePaginationButtonHoverBackground disabled:opacity-50"
|
|
88
|
+
@click="switchPage(1); pageInput = currentPage.toString();" :disabled="currentPage <= 1">
|
|
89
|
+
<!-- <IconChevronDoubleLeftOutline class="w-4 h-4" /> -->
|
|
90
|
+
1
|
|
91
|
+
</button>
|
|
92
|
+
<div
|
|
93
|
+
contenteditable="true"
|
|
94
|
+
class="min-w-10 outline-none inline-block w-auto py-1.5 px-3 text-sm text-center text-lightTablePaginationInputText border border-lightTablePaginationInputBorder bg-lightTablePaginationInputBackground dark:border-darkTablePaginationInputBorder dark:text-darkTablePaginationInputText dark:bg-darkTablePaginationInputBackground z-10"
|
|
95
|
+
@keydown="onPageKeydown($event)"
|
|
96
|
+
@input="onPageInput($event)"
|
|
97
|
+
@blur="validatePageInput()"
|
|
98
|
+
>
|
|
99
|
+
{{ pageInput }}
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<button
|
|
103
|
+
class="flex items-center py-1 px-3 text-sm font-medium text-lightUnactivePaginationButtonText focus:outline-none bg-lightUnactivePaginationButtonBackground border-l-0 border border-lightUnactivePaginationButtonBorder hover:bg-lightUnactivePaginationButtonHoverBackground hover:text-lightUnactivePaginationButtonHoverText dark:bg-darkUnactivePaginationButtonBackground dark:text-darkUnactivePaginationButtonText dark:border-darkUnactivePaginationButtonBorder dark:hover:text-darkUnactivePaginationButtonHoverText dark:hover:bg-darkUnactivePaginationButtonHoverBackground disabled:opacity-50"
|
|
104
|
+
@click="currentPage = totalPages; pageInput = currentPage.toString();" :disabled="currentPage >= totalPages">
|
|
105
|
+
{{ totalPages }}
|
|
106
|
+
|
|
107
|
+
<!-- <IconChevronDoubleRightOutline class="w-4 h-4" /> -->
|
|
108
|
+
</button>
|
|
109
|
+
<button
|
|
110
|
+
class="flex items-center py-1 px-3 gap-1 text-sm font-medium text-lightActivePaginationButtonText focus:outline-none bg-lightActivePaginationButtonBackground border-l-0 rounded-e hover:opacity-90 dark:bg-darkActivePaginationButtonBackground dark:text-darkActivePaginationButtonText disabled:opacity-50"
|
|
111
|
+
@click="currentPage++; pageInput = currentPage.toString();" :disabled="currentPage >= totalPages">
|
|
112
|
+
<svg class="w-3.5 h-3.5 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none"
|
|
113
|
+
viewBox="0 0 14 10">
|
|
114
|
+
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
115
|
+
d="M1 5h12m0 0L9 1m4 4L9 9"/>
|
|
116
|
+
</svg>
|
|
117
|
+
</button>
|
|
99
118
|
</div>
|
|
100
|
-
|
|
101
|
-
<button
|
|
102
|
-
class="flex items-center py-1 px-3 text-sm font-medium text-lightUnactivePaginationButtonText focus:outline-none bg-lightUnactivePaginationButtonBackground border-l-0 border border-lightUnactivePaginationButtonBorder hover:bg-lightUnactivePaginationButtonHoverBackground hover:text-lightUnactivePaginationButtonHoverText dark:bg-darkUnactivePaginationButtonBackground dark:text-darkUnactivePaginationButtonText dark:border-darkUnactivePaginationButtonBorder dark:hover:text-darkUnactivePaginationButtonHoverText dark:hover:bg-darkUnactivePaginationButtonHoverBackground disabled:opacity-50"
|
|
103
|
-
@click="currentPage = totalPages; pageInput = currentPage.toString();" :disabled="currentPage >= totalPages">
|
|
104
|
-
{{ totalPages }}
|
|
105
|
-
|
|
106
|
-
<!-- <IconChevronDoubleRightOutline class="w-4 h-4" /> -->
|
|
107
|
-
</button>
|
|
108
|
-
<button
|
|
109
|
-
class="flex items-center py-1 px-3 gap-1 text-sm font-medium text-lightActivePaginationButtonText focus:outline-none bg-lightActivePaginationButtonBackground border-l-0 rounded-e hover:opacity-90 dark:bg-darkActivePaginationButtonBackground dark:text-darkActivePaginationButtonText disabled:opacity-50"
|
|
110
|
-
@click="currentPage++; pageInput = currentPage.toString();" :disabled="currentPage >= totalPages">
|
|
111
|
-
<svg class="w-3.5 h-3.5 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none"
|
|
112
|
-
viewBox="0 0 14 10">
|
|
113
|
-
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
114
|
-
d="M1 5h12m0 0L9 1m4 4L9 9"/>
|
|
115
|
-
</svg>
|
|
116
|
-
</button>
|
|
117
119
|
</div>
|
|
118
|
-
</
|
|
119
|
-
</
|
|
120
|
-
</div>
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
120
|
+
</nav>
|
|
121
|
+
</div>
|
|
124
122
|
</template>
|
|
125
123
|
|
|
126
124
|
<script setup lang="ts">
|
|
127
|
-
import { ref, computed, useTemplateRef, watch,
|
|
125
|
+
import { ref, computed, useTemplateRef, watch, onMounted } from 'vue';
|
|
128
126
|
import SkeleteLoader from '@/components/SkeleteLoader.vue';
|
|
129
127
|
import { IconTableRowOutline } from '@iconify-prerendered/vue-flowbite';
|
|
130
128
|
|
|
@@ -132,53 +130,6 @@
|
|
|
132
130
|
refreshTable
|
|
133
131
|
})
|
|
134
132
|
|
|
135
|
-
type Row = Record<string, unknown>
|
|
136
|
-
type LoadFn = (params: { offset: number, limit: number }) => Promise<{ data: Row[]; total: number }>
|
|
137
|
-
|
|
138
|
-
const isFunc = (v: unknown): v is LoadFn => typeof v === 'function'
|
|
139
|
-
|
|
140
|
-
function usePagedData(props: {
|
|
141
|
-
data: Row[] | LoadFn
|
|
142
|
-
pageSize: number
|
|
143
|
-
currentPage: number
|
|
144
|
-
}) {
|
|
145
|
-
const page = ref(props.currentPage)
|
|
146
|
-
const pageSize = toRef(props, 'pageSize')
|
|
147
|
-
|
|
148
|
-
const isLoading = ref(false)
|
|
149
|
-
const error = shallowRef<unknown>(null)
|
|
150
|
-
const result = shallowRef<{ data: Row[]; total: number }>({ data: [], total: 0 })
|
|
151
|
-
|
|
152
|
-
let requestId = 0
|
|
153
|
-
|
|
154
|
-
async function fetchData() {
|
|
155
|
-
const id = ++requestId
|
|
156
|
-
isLoading.value = true
|
|
157
|
-
error.value = null
|
|
158
|
-
try {
|
|
159
|
-
if (isFunc(props.data)) {
|
|
160
|
-
const res = await props.data({offset: ((page.value - 1) * pageSize.value), limit: pageSize.value})
|
|
161
|
-
if (id !== requestId) return
|
|
162
|
-
result.value = res
|
|
163
|
-
} else {
|
|
164
|
-
const start = (page.value - 1) * pageSize.value
|
|
165
|
-
const end = start + pageSize.value
|
|
166
|
-
result.value = { data: props.data.slice(start, end), total: props.data.length }
|
|
167
|
-
}
|
|
168
|
-
} catch (e) {
|
|
169
|
-
if (id !== requestId) return
|
|
170
|
-
error.value = e
|
|
171
|
-
result.value = { data: [], total: 0 }
|
|
172
|
-
} finally {
|
|
173
|
-
if (id === requestId) isLoading.value = false
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
watch([page, pageSize, () => props.data], fetchData, { immediate: true })
|
|
178
|
-
|
|
179
|
-
return { page, pageSize, isLoading, error, result, refresh: fetchData }
|
|
180
|
-
}
|
|
181
|
-
|
|
182
133
|
const props = withDefaults(
|
|
183
134
|
defineProps<{
|
|
184
135
|
columns: {
|
|
@@ -196,17 +147,23 @@
|
|
|
196
147
|
}
|
|
197
148
|
);
|
|
198
149
|
|
|
199
|
-
const { result: dataResult, isLoading, error, page: currentPage, pageSize, refresh } = usePagedData({
|
|
200
|
-
data: props.data,
|
|
201
|
-
pageSize: props.pageSize,
|
|
202
|
-
currentPage: 1
|
|
203
|
-
});
|
|
204
|
-
|
|
205
150
|
const pageInput = ref('1');
|
|
206
151
|
const rowRefs = useTemplateRef<HTMLElement[]>('rowRefs');
|
|
207
152
|
const headerRefs = useTemplateRef<HTMLElement[]>('headerRefs');
|
|
208
153
|
const rowHeights = ref<number[]>([]);
|
|
209
154
|
const columnWidths = ref<number[]>([]);
|
|
155
|
+
const currentPage = ref(1);
|
|
156
|
+
const isLoading = ref(false);
|
|
157
|
+
const dataResult = ref<{data: {[key: string]: any}[], total: number}>({data: [], total: 0});
|
|
158
|
+
const isAtLeastOneLoading = ref<boolean[]>([false]);
|
|
159
|
+
|
|
160
|
+
onMounted(() => {
|
|
161
|
+
refresh();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
watch( currentPage, async () => {
|
|
165
|
+
refresh();
|
|
166
|
+
});
|
|
210
167
|
|
|
211
168
|
watch(() => currentPage.value, () => {
|
|
212
169
|
rowHeights.value = !rowRefs.value ? [] : rowRefs.value.map((el: HTMLElement) => el.offsetHeight);
|
|
@@ -218,7 +175,7 @@
|
|
|
218
175
|
});
|
|
219
176
|
|
|
220
177
|
const dataPage = computed(() => {
|
|
221
|
-
return dataResult.value
|
|
178
|
+
return dataResult.value?.data;
|
|
222
179
|
});
|
|
223
180
|
|
|
224
181
|
function switchPage(p: number) {
|
|
@@ -258,9 +215,30 @@
|
|
|
258
215
|
}
|
|
259
216
|
}
|
|
260
217
|
|
|
218
|
+
async function refresh() {
|
|
219
|
+
if (typeof props.data === 'function') {
|
|
220
|
+
isLoading.value = true;
|
|
221
|
+
const currentLoadingIndex = currentPage.value;
|
|
222
|
+
isAtLeastOneLoading.value[currentLoadingIndex] = true;
|
|
223
|
+
const result = await props.data({ offset: (currentLoadingIndex - 1) * props.pageSize, limit: props.pageSize });
|
|
224
|
+
isAtLeastOneLoading.value[currentLoadingIndex] = false;
|
|
225
|
+
if (isAtLeastOneLoading.value.every(v => v === false)) {
|
|
226
|
+
isLoading.value = false;
|
|
227
|
+
}
|
|
228
|
+
dataResult.value = result;
|
|
229
|
+
} else {
|
|
230
|
+
const start = (currentPage.value - 1) * props.pageSize;
|
|
231
|
+
const end = start + props.pageSize;
|
|
232
|
+
dataResult.value = { data: props.data.slice(start, end), total: props.data.length };
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
261
236
|
function refreshTable() {
|
|
262
|
-
currentPage.value
|
|
263
|
-
|
|
237
|
+
if ( currentPage.value !== 1 ) {
|
|
238
|
+
currentPage.value = 1;
|
|
239
|
+
} else {
|
|
240
|
+
refresh();
|
|
241
|
+
}
|
|
264
242
|
}
|
|
265
243
|
|
|
266
244
|
</script>
|