v-sistec-features 1.2.5 → 1.3.0

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,519 @@
1
+ <template>
2
+ <div>
3
+ <div v-if="type_fetch === 'pagination'" class="" :class="props.class_container">
4
+ <template v-for="item in items" :key="item[props.item_key]">
5
+ <slot name="body" :item="item">
6
+
7
+ </slot>
8
+ </template>
9
+ </div>
10
+ <div v-else-if="type_fetch === 'infinite-scroll'" :class="props.class_container">
11
+
12
+ <InfiniteLoading :identifier="topLoaderId" direction="top" @infinite="carregarPaginaTop">
13
+ <template #complete><span></span></template>
14
+ <template #spinner><span></span></template>
15
+ </InfiniteLoading>
16
+ <template v-for="item in items_infinite" :key="item[props.item_key]">
17
+ <slot name="body" :item="item">
18
+ </slot>
19
+ </template>
20
+ <InfiniteLoading :identifier="bottomLoaderId" @infinite="carregarPaginaBottom">
21
+ <template #complete>
22
+ <slot name="scroll-finish">
23
+ <span class="scroll-finish-style">
24
+ Fim dos registros</span>
25
+ </slot>
26
+ </template>
27
+ </InfiniteLoading>
28
+ </div>
29
+ <slot v-if="type_fetch === 'pagination'" name="pagination" :pagination="pagination"
30
+ :tradePage="fetchDataWithDelay" :error="error">
31
+ <div v-if="!error && pagination.count > 0" class="px-3" :class="props.class_pagination">
32
+ <PaginationDatatable :filtering="true" :pagination="pagination" @tradePage="fetchDataWithDelay" />
33
+ </div>
34
+ </slot>
35
+ </div>
36
+
37
+ </template>
38
+
39
+ <script setup lang="ts" generic="T extends Record<string, any>">
40
+ import { readonly, ref, isRef, computed, watch, onMounted, nextTick, type Component, type Ref, type WatchSource } from 'vue';
41
+ import InfiniteLoading from "v3-infinite-loading";
42
+ import PaginationDatatable from './PaginationDatatable.vue';
43
+ // import Search from './SearchDatatable.vue';
44
+
45
+ interface VDataPageProps {
46
+ /* configuração do useApiFetch */
47
+ fetch: Function;
48
+ fetch_name?: string;
49
+ endpoint: string;
50
+ /* tipos de loading pré-definidos*/
51
+ type_loading?: 'placeholder' | 'spiner-table' | 'spiner';
52
+ type_fetch?: 'pagination' | 'infinite-scroll' | 'none';
53
+
54
+ /*recebe um component para loading*/
55
+ custom_loading?: Component | null;
56
+ /* retira os params default da requisição */
57
+ deactivate_default_params?: boolean;
58
+ /* nomes dos parâmetros para passar para o backend */
59
+ filter_param_name?: string;
60
+ search_param_name?: string;
61
+ page_param_name?: string;
62
+ page_size_param_name?: string;
63
+ add_params?: Object | Function;
64
+
65
+ /* usado para pegar os dados do useApiFetch */
66
+ data_key?: string;
67
+ total_key?: string;
68
+
69
+ /* filtros que irão ser usados */
70
+ list_filter?: any[];
71
+ /* mudar o que está escrito no select de mudança de items_per_page*/
72
+ first_text_page_size?: string;
73
+ second_text_page_size?: string;
74
+
75
+
76
+ /* props para estilizar o vdatatable */
77
+ class_container?: string;
78
+ class_pagination?: string;
79
+ class_filters?: string;
80
+
81
+ /*
82
+ * tempo mínimo em ms para mostrar o loading para evitar telas piscando
83
+ */
84
+ min_loading_delay?: number;
85
+ /*
86
+ - Número de tentativas automáticas em caso de falha.
87
+ - 1 significa que a requisição será feita apenas uma vez, sem retentativas.
88
+ - Valor padrão é 3.
89
+ */
90
+ retry_attempts?: number;
91
+ // Atraso em milissegundos entre cada tentativa
92
+ retry_delay?: number;
93
+
94
+ // Ativa a funcionalidade de seleção com checkboxes
95
+ use_checkbox?: boolean;
96
+ // Define qual propriedade do item será usada como chave única para a seleção.
97
+ item_key?: string;
98
+
99
+ limit_per_page?: number;
100
+
101
+ next_page_response_name?: string;
102
+ page_starts_at: number;
103
+ element_id?: string;
104
+ watch?: WatchSource[]
105
+ }
106
+
107
+ interface ExposedFunctions {
108
+ execute: () => void;
109
+ pagination: Ref<PaginationObject>;
110
+ default_params: Record<string, any>;
111
+ set_limit_per_page: (newLimit: number) => void;
112
+ set_search: (newSearch: string) => void;
113
+ set_filter: (newFilter: string) => void;
114
+ set_page: (newPage: number) => void;
115
+ }
116
+
117
+ // =======================================================
118
+ // 1. DEFINIÇÃO DE PROPS COM VALORES PADRÃO
119
+ // =======================================================
120
+
121
+ const props = withDefaults(defineProps<VDataPageProps>(), {
122
+ fetch_name: '',
123
+ type_loading: 'placeholder',
124
+ custom_loading: null,
125
+ deactivate_default_params: false,
126
+ filter_param_name: 'filter',
127
+ search_param_name: 'search',
128
+ page_param_name: 'page',
129
+ page_size_param_name: 'page_size',
130
+
131
+ next_page_response_name: 'next_page',
132
+ add_params: () => ({}),
133
+ data_key: 'results',
134
+ total_key: 'count',
135
+ list_filter: () => [],
136
+ class_container: '',
137
+ class_pagination: '',
138
+ class_filters: '',
139
+ min_loading_delay: 600,
140
+ retry_attempts: 3,
141
+ retry_delay: 2000,
142
+ use_checkbox: false,
143
+ item_key: 'id',
144
+ first_text_page_size: 'Mostrar',
145
+ second_text_page_size: 'registros',
146
+ limit_per_page: 5,
147
+ type_fetch: 'pagination',
148
+ page_starts_at: 0,
149
+ element_id: '',
150
+ watch: () => []
151
+ });
152
+
153
+
154
+ // =======================================================
155
+ // 2. ESTADO REATIVO PRINCIPAL
156
+ // =======================================================
157
+ const first_fetch = ref<boolean>(true);
158
+ const bottomLoaderId = ref(Date.now());
159
+ const topLoaderId = ref(Date.now());
160
+
161
+ const items = ref<T[]>([]) as Ref<T[]>;
162
+ const items_infinite = ref<T[]>([]) as Ref<T[]>;
163
+
164
+ const totalItems = ref<number>(0);
165
+ const isDelaying = ref<boolean>(false);
166
+ const delayTimer = ref<ReturnType<typeof setTimeout> | null>(null);
167
+ const dadosInicializados = ref<boolean>(false)
168
+
169
+ interface PaginationObject {
170
+ current_page: number;
171
+ count: number;
172
+ limit_per_page: number;
173
+ search: string;
174
+ filter: string;
175
+ }
176
+
177
+ /*--------- definição de páginação ---------------*/
178
+ const pagination = ref<PaginationObject>({
179
+ current_page: props.page_starts_at, // pagina atual
180
+ count: 0, // total de itens
181
+ limit_per_page: props.limit_per_page, // limite de itens por página
182
+ search: '', // termo de busca
183
+ filter: '', // filtro selecionado
184
+
185
+ })
186
+
187
+ // =======================================================
188
+ // 3. LÓGICA DA API (useFetch)
189
+ // =======================================================
190
+ const { data: response, pending: _pending, error, execute, attempt: _attempt } = props.fetch(props.endpoint, {
191
+ params: () => {
192
+
193
+ if (props.deactivate_default_params) {
194
+ if (props.add_params && typeof props.add_params === 'function') {
195
+ return props.add_params();
196
+ }
197
+ return {
198
+ ...props.add_params,
199
+ };
200
+ }
201
+ else if (props.add_params && typeof props.add_params === 'function') {
202
+ return {
203
+ ...default_params.value,
204
+ ...props.add_params(),
205
+ }
206
+ }
207
+ return {
208
+ ...default_params.value,
209
+ ...props.add_params,
210
+ };
211
+ },
212
+ retry: props.retry_attempts,
213
+ retryDelay: props.retry_delay,
214
+ paramsReactives: false,
215
+ immediate: false,
216
+ }, props.fetch_name);
217
+
218
+ // =======================================================
219
+ // 4. PROPRIEDADES COMPUTADAS
220
+ // =======================================================
221
+ // const item_use = computed<number[]>(() => {
222
+ // let use = [1]
223
+ // if (props.list_filter.length > 0) {
224
+ // use.push(2)
225
+ // }
226
+ // return use;
227
+ // });
228
+
229
+ const default_params = computed<Record<string, any>>(() => ({
230
+ [props.page_param_name]: pagination.value.current_page + 1,
231
+ [props.page_size_param_name]: pagination.value.limit_per_page,
232
+ [props.search_param_name]: pagination.value.search || "",
233
+ [props.filter_param_name]: pagination.value.filter || "",
234
+ }));
235
+
236
+ // para controlar a exibição do loading
237
+ // const showLoadingState = computed<boolean>(() => {
238
+ // return (pending.value || isDelaying.value)
239
+ // });
240
+
241
+
242
+
243
+ // =======================================================
244
+ // 5. WATCHERS (Observadores)
245
+ // =======================================================
246
+
247
+
248
+
249
+ watch(response, (newResponse: any) => {
250
+ if (newResponse) {
251
+ items.value = newResponse[props.data_key] || [];
252
+ totalItems.value = newResponse[props.total_key] || 0;
253
+ pagination.value.count = totalItems.value;
254
+ } else {
255
+ items.value = [];
256
+ totalItems.value = 0;
257
+ }
258
+ }, { immediate: true });
259
+
260
+
261
+ // =======================================================
262
+ // 6. MÉTODOS
263
+ // =======================================================
264
+
265
+ // Função que gerencia o delay e a chamada da API
266
+ async function fetchDataWithDelay(): Promise<void> {
267
+ // Limpa timer anterior, se houver
268
+ if (delayTimer.value) clearTimeout(delayTimer.value);
269
+
270
+ isDelaying.value = true;
271
+
272
+ delayTimer.value = setTimeout(() => {
273
+ isDelaying.value = false;
274
+ }, props.min_loading_delay);
275
+
276
+ return execute(); // Executa a busca de dados original do useApiFetch
277
+ }
278
+ async function initDataInfinite() {
279
+ items.value = [];
280
+ items_infinite.value = [];
281
+
282
+ pagination.value.current_page = props.page_starts_at;
283
+
284
+ await fetchDataWithDelay();
285
+
286
+ nextTick(() => {
287
+ items_infinite.value.push(...items.value);
288
+ dadosInicializados.value = true;
289
+ bottomLoaderId.value++;
290
+ topLoaderId.value++;
291
+ });
292
+ }
293
+
294
+
295
+ // function reSearch(): void {
296
+ // pagination.value.current_page = 0;
297
+ // fetchDataWithDelay();
298
+ // }
299
+
300
+ // const changePageSize = (event: Event): void => {
301
+ // const target = event.target as HTMLInputElement;
302
+ // const newSize = parseInt(target.value, 10);
303
+ // if (newSize > 0) {
304
+ // pagination.value.limit_per_page = newSize;
305
+ // pagination.value.limit_per_page = newSize; // Atualiza o limite de itens por página
306
+ // pagination.value.current_page = 0;
307
+ // fetchDataWithDelay();
308
+ // }
309
+ // };
310
+
311
+
312
+
313
+ // =======================================================
314
+ // 7. EXPOSE E CICLO DE VIDA
315
+ // =======================================================
316
+ function set_limit_per_page(newLimit: number): void {
317
+ if (newLimit > 0) {
318
+ pagination.value.limit_per_page = newLimit;
319
+ pagination.value.current_page = 0;
320
+ fetchDataWithDelay();
321
+ } else {
322
+ console.warn("O limite deve ser um número maior que zero.");
323
+ }
324
+ }
325
+ function set_search(newSearch: string): void {
326
+ pagination.value.search = newSearch;
327
+ pagination.value.current_page = 0;
328
+ fetchDataWithDelay();
329
+ }
330
+ function set_filter(newFilter: string): void {
331
+ pagination.value.filter = newFilter;
332
+ pagination.value.current_page = 0;
333
+ fetchDataWithDelay();
334
+ }
335
+ function set_page(newPage: number): void {
336
+ if (newPage >= 1 && newPage <= Math.ceil(pagination.value.count / pagination.value.limit_per_page)) {
337
+ pagination.value.current_page = newPage - 1;
338
+ fetchDataWithDelay();
339
+ } else {
340
+ console.warn("Número de página inválido.");
341
+ }
342
+ }
343
+
344
+ defineExpose<
345
+ ExposedFunctions
346
+ >({
347
+ execute: fetchDataWithDelay,
348
+ pagination: readonly(pagination),
349
+ set_limit_per_page: set_limit_per_page,
350
+ set_search: set_search,
351
+ set_filter: set_filter,
352
+ set_page: set_page,
353
+ default_params
354
+ });
355
+
356
+ onMounted(() => {
357
+ nextTick(() => {
358
+
359
+ /*
360
+ * executar dentro do nextTick para garantir que o pai já tem acesso ao
361
+ * ref que foi exposto
362
+ */
363
+ if (first_fetch.value && props.type_fetch === 'infinite-scroll') {
364
+ initDataInfinite();
365
+ first_fetch.value = false;
366
+ } else if (first_fetch.value && props.type_fetch === 'pagination') {
367
+ fetchDataWithDelay();
368
+ first_fetch.value = false;
369
+ }
370
+
371
+ })
372
+ });
373
+ const proxima_pagina = computed(() => {
374
+ return response.value?.[props.next_page_response_name] || null
375
+ })
376
+
377
+
378
+ async function carregarPaginaBottom($state: any) {
379
+ if (!dadosInicializados.value) return;
380
+
381
+
382
+ if (!proxima_pagina.value) {
383
+ $state.complete();
384
+ return;
385
+ }
386
+ pagination.value.current_page += 1;
387
+ // paginaAtual.value += 1;
388
+ await execute();
389
+
390
+ if (error.value) {
391
+ $state.error();
392
+ return;
393
+ }
394
+ if (!items.value || items.value.length === 0) {
395
+ $state.complete();
396
+ return;
397
+ }
398
+
399
+ // --- FILTRAR DUPLICATAS ---
400
+ const novosItens = items.value.filter(
401
+ (novoItem: any) => !items_infinite.value.some(itemExistente => itemExistente[props.item_key] === novoItem[props.item_key])
402
+ );
403
+ if (novosItens.length === 0 && items.value.length > 0) {
404
+ $state.loaded();
405
+ return;
406
+ }
407
+
408
+ items_infinite.value.push(...novosItens);
409
+
410
+ nextTick(() => {
411
+ if (items_infinite.value && items_infinite.value.length > pagination.value.limit_per_page * 2) {
412
+ items_infinite.value.splice(0, pagination.value.limit_per_page);
413
+ }
414
+ if (proxima_pagina.value) {
415
+ $state.loaded();
416
+ } else {
417
+ $state.complete();
418
+ }
419
+ topLoaderId.value++;
420
+ });
421
+ }
422
+ async function carregarPaginaTop($state: any) {
423
+ if (!dadosInicializados.value) return;
424
+
425
+ if (pagination.value.current_page <= props.page_starts_at) {
426
+ $state.complete();
427
+ return;
428
+ }
429
+ const primeiro_item = items_infinite.value[0];
430
+ if (!primeiro_item) return;
431
+
432
+ const id_primeiro_artigo = primeiro_item[props.item_key];
433
+
434
+ if (!id_primeiro_artigo) {
435
+ $state.error();
436
+ return
437
+ };
438
+
439
+ pagination.value.current_page -= 1;
440
+ await execute();
441
+
442
+ if (error.value) {
443
+ $state.error();
444
+ return;
445
+ }
446
+
447
+ if (!items.value || items.value.length === 0) {
448
+ $state.loaded();
449
+ return;
450
+ }
451
+
452
+ // --- FILTRAR DUPLICATAS ---
453
+ const novosItens = items.value.filter(
454
+ (novoItem: any) => !items_infinite.value.some(itemExistente => itemExistente[props.item_key] === novoItem[props.item_key])
455
+ );
456
+ if (novosItens.length === 0 && items.value.length > 0) {
457
+ $state.loaded();
458
+ return; // Evita adicionar duplicatas
459
+ }
460
+
461
+ items_infinite.value.unshift(...novosItens);
462
+
463
+ if (items_infinite.value && items_infinite.value.length > pagination.value.limit_per_page * 2) {
464
+ items_infinite.value.splice(-pagination.value.limit_per_page, pagination.value.limit_per_page);
465
+ }
466
+ nextTick(() => {
467
+
468
+ const elementoAlvo = document.getElementById(props.element_id + id_primeiro_artigo);
469
+ if (elementoAlvo) {
470
+ elementoAlvo.scrollIntoView({ behavior: 'auto', block: 'start' });
471
+ } else {
472
+ console.warn(`
473
+ Elemento não encontrado para scroll verifique se a propriedade 'element_id' está correta ou não foi definida.
474
+ `);
475
+ }
476
+ bottomLoaderId.value++;
477
+ $state.loaded();
478
+ });
479
+ }
480
+
481
+
482
+ const watchSources: WatchSource[] = [];
483
+
484
+ if (props.watch && Array.isArray(props.watch)) {
485
+ props.watch.forEach(source => {
486
+ if (isRef(source) || typeof source === 'function') {
487
+ watchSources.push(source);
488
+ }
489
+ });
490
+ }
491
+ if (watchSources.length > 0) {
492
+ if (props.type_fetch === 'pagination') {
493
+ watch(watchSources, () => {
494
+ pagination.value.current_page = props.page_starts_at;
495
+ fetchDataWithDelay();
496
+ }, { deep: true });
497
+ } else if (props.type_fetch === 'infinite-scroll') {
498
+ watch(watchSources, () =>{
499
+ dadosInicializados.value = false;
500
+ initDataInfinite();
501
+ } , { deep: true });
502
+ }
503
+
504
+ }
505
+
506
+ </script>
507
+
508
+ <style lang="scss" scoped>
509
+ .scroll-finish-style {
510
+ color: #6c757d;
511
+
512
+ display: block;
513
+
514
+ text-align: center;
515
+
516
+ margin-top: 1rem;
517
+ margin-bottom: 1rem;
518
+ }
519
+ </style>
@@ -0,0 +1,4 @@
1
+ import VDataPage from "./components/VDataPage.vue";
2
+ export {
3
+ VDataPage
4
+ };
@@ -3,7 +3,7 @@
3
3
  <div class="" :class="props.class_container">
4
4
  <slot></slot>
5
5
  <div class="" :class="props.class_content">
6
- <div :class="props.class_filters" class="d-flex justify-content-between align-items-start mb-2">
6
+ <div :class="props.class_filters" class="d-flex justify-content-between align-items-start ">
7
7
  <slot name="pageSize" :changePageSize="changePageSize" :limit_per_page="pagination.limit_per_page">
8
8
  <div class="text-secondary">
9
9
  {{ props.first_text_page_size }}