v-sistec-features 1.2.6 → 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.
@@ -0,0 +1,529 @@
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
+ scroll_on_trade_page?: boolean;
106
+ }
107
+
108
+ interface ExposedFunctions {
109
+ execute: () => void;
110
+ pagination: Ref<PaginationObject>;
111
+ default_params: Record<string, any>;
112
+ set_limit_per_page: (newLimit: number) => void;
113
+ set_search: (newSearch: string) => void;
114
+ set_filter: (newFilter: string) => void;
115
+ set_page: (newPage: number) => void;
116
+
117
+
118
+ }
119
+
120
+ // =======================================================
121
+ // 1. DEFINIÇÃO DE PROPS COM VALORES PADRÃO
122
+ // =======================================================
123
+
124
+ const props = withDefaults(defineProps<VDataPageProps>(), {
125
+ fetch_name: '',
126
+ type_loading: 'placeholder',
127
+ custom_loading: null,
128
+ deactivate_default_params: false,
129
+ filter_param_name: 'filter',
130
+ search_param_name: 'search',
131
+ page_param_name: 'page',
132
+ page_size_param_name: 'page_size',
133
+
134
+ next_page_response_name: 'next_page',
135
+ add_params: () => ({}),
136
+ data_key: 'results',
137
+ total_key: 'count',
138
+ list_filter: () => [],
139
+ class_container: '',
140
+ class_pagination: '',
141
+ class_filters: '',
142
+ min_loading_delay: 600,
143
+ retry_attempts: 3,
144
+ retry_delay: 2000,
145
+ use_checkbox: false,
146
+ item_key: 'id',
147
+ first_text_page_size: 'Mostrar',
148
+ second_text_page_size: 'registros',
149
+ limit_per_page: 5,
150
+ type_fetch: 'pagination',
151
+ page_starts_at: 0,
152
+ element_id: '',
153
+ scroll_on_trade_page: false,
154
+ watch: () => []
155
+ });
156
+
157
+
158
+ // =======================================================
159
+ // 2. ESTADO REATIVO PRINCIPAL
160
+ // =======================================================
161
+ const first_fetch = ref<boolean>(true);
162
+ const bottomLoaderId = ref(Date.now());
163
+ const topLoaderId = ref(Date.now());
164
+
165
+ const items = ref<T[]>([]) as Ref<T[]>;
166
+ const items_infinite = ref<T[]>([]) as Ref<T[]>;
167
+
168
+ const totalItems = ref<number>(0);
169
+ const isDelaying = ref<boolean>(false);
170
+ const delayTimer = ref<ReturnType<typeof setTimeout> | null>(null);
171
+ const dadosInicializados = ref<boolean>(false)
172
+
173
+ interface PaginationObject {
174
+ current_page: number;
175
+ count: number;
176
+ limit_per_page: number;
177
+ search: string;
178
+ filter: string;
179
+ }
180
+
181
+ /*--------- definição de páginação ---------------*/
182
+ const pagination = ref<PaginationObject>({
183
+ current_page: props.page_starts_at, // pagina atual
184
+ count: 0, // total de itens
185
+ limit_per_page: props.limit_per_page, // limite de itens por página
186
+ search: '', // termo de busca
187
+ filter: '', // filtro selecionado
188
+
189
+ })
190
+
191
+ // =======================================================
192
+ // 3. LÓGICA DA API (useFetch)
193
+ // =======================================================
194
+ const { data: response, pending: _pending, error, execute, attempt: _attempt } = props.fetch(props.endpoint, {
195
+ params: () => {
196
+
197
+ if (props.deactivate_default_params) {
198
+ if (props.add_params && typeof props.add_params === 'function') {
199
+ return props.add_params();
200
+ }
201
+ return {
202
+ ...props.add_params,
203
+ };
204
+ }
205
+ else if (props.add_params && typeof props.add_params === 'function') {
206
+ return {
207
+ ...default_params.value,
208
+ ...props.add_params(),
209
+ }
210
+ }
211
+ return {
212
+ ...default_params.value,
213
+ ...props.add_params,
214
+ };
215
+ },
216
+ retry: props.retry_attempts,
217
+ retryDelay: props.retry_delay,
218
+ paramsReactives: false,
219
+ immediate: false,
220
+ }, props.fetch_name);
221
+
222
+ // =======================================================
223
+ // 4. PROPRIEDADES COMPUTADAS
224
+ // =======================================================
225
+ // const item_use = computed<number[]>(() => {
226
+ // let use = [1]
227
+ // if (props.list_filter.length > 0) {
228
+ // use.push(2)
229
+ // }
230
+ // return use;
231
+ // });
232
+
233
+ const default_params = computed<Record<string, any>>(() => ({
234
+ [props.page_param_name]: pagination.value.current_page + 1,
235
+ [props.page_size_param_name]: pagination.value.limit_per_page,
236
+ [props.search_param_name]: pagination.value.search || "",
237
+ [props.filter_param_name]: pagination.value.filter || "",
238
+ }));
239
+
240
+ // para controlar a exibição do loading
241
+ // const showLoadingState = computed<boolean>(() => {
242
+ // return (pending.value || isDelaying.value)
243
+ // });
244
+
245
+
246
+
247
+ // =======================================================
248
+ // 5. WATCHERS (Observadores)
249
+ // =======================================================
250
+
251
+
252
+
253
+ watch(response, (newResponse: any) => {
254
+ if (newResponse) {
255
+ items.value = newResponse[props.data_key] || [];
256
+ totalItems.value = newResponse[props.total_key] || 0;
257
+ pagination.value.count = totalItems.value;
258
+ } else {
259
+ items.value = [];
260
+ totalItems.value = 0;
261
+ }
262
+ }, { immediate: true });
263
+
264
+
265
+ // =======================================================
266
+ // 6. MÉTODOS
267
+ // =======================================================
268
+
269
+ // Função que gerencia o delay e a chamada da API
270
+ async function fetchDataWithDelay(): Promise<void> {
271
+ // Limpa timer anterior, se houver
272
+ if (delayTimer.value) clearTimeout(delayTimer.value);
273
+
274
+ isDelaying.value = true;
275
+
276
+ delayTimer.value = setTimeout(() => {
277
+ isDelaying.value = false;
278
+ }, props.min_loading_delay);
279
+
280
+ return execute(); // Executa a busca de dados original do useApiFetch
281
+ }
282
+ async function initDataInfinite() {
283
+ items.value = [];
284
+ items_infinite.value = [];
285
+
286
+ pagination.value.current_page = props.page_starts_at;
287
+
288
+ await fetchDataWithDelay();
289
+
290
+ nextTick(() => {
291
+ items_infinite.value.push(...items.value);
292
+ dadosInicializados.value = true;
293
+ bottomLoaderId.value++;
294
+ topLoaderId.value++;
295
+ });
296
+ }
297
+
298
+
299
+ // function reSearch(): void {
300
+ // pagination.value.current_page = 0;
301
+ // fetchDataWithDelay();
302
+ // }
303
+
304
+ // const changePageSize = (event: Event): void => {
305
+ // const target = event.target as HTMLInputElement;
306
+ // const newSize = parseInt(target.value, 10);
307
+ // if (newSize > 0) {
308
+ // pagination.value.limit_per_page = newSize;
309
+ // pagination.value.limit_per_page = newSize; // Atualiza o limite de itens por página
310
+ // pagination.value.current_page = 0;
311
+ // fetchDataWithDelay();
312
+ // }
313
+ // };
314
+
315
+
316
+
317
+ // =======================================================
318
+ // 7. EXPOSE E CICLO DE VIDA
319
+ // =======================================================
320
+ function set_limit_per_page(newLimit: number): void {
321
+ if (newLimit > 0) {
322
+ pagination.value.limit_per_page = newLimit;
323
+ pagination.value.current_page = 0;
324
+ fetchDataWithDelay();
325
+ } else {
326
+ console.warn("O limite deve ser um número maior que zero.");
327
+ }
328
+ }
329
+ function set_search(newSearch: string): void {
330
+ pagination.value.search = newSearch;
331
+ pagination.value.current_page = 0;
332
+ fetchDataWithDelay();
333
+ }
334
+ function set_filter(newFilter: string): void {
335
+ pagination.value.filter = newFilter;
336
+ pagination.value.current_page = 0;
337
+ fetchDataWithDelay();
338
+ }
339
+ function set_page(newPage: number): void {
340
+ if (newPage >= 1 && newPage <= Math.ceil(pagination.value.count / pagination.value.limit_per_page)) {
341
+ pagination.value.current_page = newPage - 1;
342
+ fetchDataWithDelay();
343
+ } else {
344
+ console.warn("Número de página inválido.");
345
+ }
346
+ }
347
+
348
+ defineExpose<
349
+ ExposedFunctions
350
+ >({
351
+ execute: fetchDataWithDelay,
352
+ pagination: readonly(pagination),
353
+ set_limit_per_page: set_limit_per_page,
354
+ set_search: set_search,
355
+ set_filter: set_filter,
356
+ set_page: set_page,
357
+ default_params
358
+ });
359
+
360
+ onMounted(() => {
361
+ nextTick(() => {
362
+
363
+ /*
364
+ * executar dentro do nextTick para garantir que o pai já tem acesso ao
365
+ * ref que foi exposto
366
+ */
367
+ if (first_fetch.value && props.type_fetch === 'infinite-scroll') {
368
+ initDataInfinite();
369
+ first_fetch.value = false;
370
+ } else if (first_fetch.value && props.type_fetch === 'pagination') {
371
+ fetchDataWithDelay();
372
+ first_fetch.value = false;
373
+ }
374
+
375
+ })
376
+ });
377
+ const proxima_pagina = computed(() => {
378
+ return response.value?.[props.next_page_response_name] || null
379
+ })
380
+
381
+
382
+ async function carregarPaginaBottom($state: any) {
383
+ if (!dadosInicializados.value) return;
384
+
385
+
386
+ if (!proxima_pagina.value) {
387
+ $state.complete();
388
+ return;
389
+ }
390
+ pagination.value.current_page += 1;
391
+ // paginaAtual.value += 1;
392
+ await execute();
393
+
394
+ if (error.value) {
395
+ $state.error();
396
+ return;
397
+ }
398
+ if (!items.value || items.value.length === 0) {
399
+ $state.complete();
400
+ return;
401
+ }
402
+
403
+ // --- FILTRAR DUPLICATAS ---
404
+ const novosItens = items.value.filter(
405
+ (novoItem: any) => !items_infinite.value.some(itemExistente => itemExistente[props.item_key] === novoItem[props.item_key])
406
+ );
407
+ if (novosItens.length === 0 && items.value.length > 0) {
408
+ $state.loaded();
409
+ return;
410
+ }
411
+
412
+ items_infinite.value.push(...novosItens);
413
+
414
+ nextTick(() => {
415
+ if (items_infinite.value && items_infinite.value.length > pagination.value.limit_per_page * 2) {
416
+ items_infinite.value.splice(0, pagination.value.limit_per_page);
417
+ }
418
+ if (proxima_pagina.value) {
419
+ $state.loaded();
420
+ } else {
421
+ $state.complete();
422
+ }
423
+ topLoaderId.value++;
424
+ });
425
+ }
426
+ async function carregarPaginaTop($state: any) {
427
+ if (!dadosInicializados.value) return;
428
+
429
+ if (pagination.value.current_page <= props.page_starts_at) {
430
+ $state.complete();
431
+ return;
432
+ }
433
+ const primeiro_item = items_infinite.value[0];
434
+ if (!primeiro_item) return;
435
+
436
+ const id_primeiro_artigo = primeiro_item[props.item_key];
437
+
438
+ if (!id_primeiro_artigo) {
439
+ $state.error();
440
+ return
441
+ };
442
+
443
+ pagination.value.current_page -= 1;
444
+ await execute();
445
+
446
+ if (error.value) {
447
+ $state.error();
448
+ return;
449
+ }
450
+
451
+ if (!items.value || items.value.length === 0) {
452
+ $state.loaded();
453
+ return;
454
+ }
455
+
456
+ // --- FILTRAR DUPLICATAS ---
457
+ const novosItens = items.value.filter(
458
+ (novoItem: any) => !items_infinite.value.some(itemExistente => itemExistente[props.item_key] === novoItem[props.item_key])
459
+ );
460
+ if (novosItens.length === 0 && items.value.length > 0) {
461
+ $state.loaded();
462
+ return; // Evita adicionar duplicatas
463
+ }
464
+
465
+ items_infinite.value.unshift(...novosItens);
466
+
467
+ if (items_infinite.value && items_infinite.value.length > pagination.value.limit_per_page * 2) {
468
+ items_infinite.value.splice(-pagination.value.limit_per_page, pagination.value.limit_per_page);
469
+ }
470
+ nextTick(() => {
471
+
472
+ const elementoAlvo = document.getElementById(props.element_id + id_primeiro_artigo);
473
+ if (elementoAlvo) {
474
+ elementoAlvo.scrollIntoView({ behavior: 'auto', block: 'start' });
475
+ } else {
476
+ console.warn(`
477
+ Elemento não encontrado para scroll verifique se a propriedade 'element_id' está correta ou não foi definida.
478
+ `);
479
+ }
480
+ bottomLoaderId.value++;
481
+ $state.loaded();
482
+ });
483
+ }
484
+
485
+
486
+ const watchSources: WatchSource[] = [];
487
+
488
+ if (props.watch && Array.isArray(props.watch)) {
489
+ props.watch.forEach(source => {
490
+ if (isRef(source) || typeof source === 'function') {
491
+ watchSources.push(source);
492
+ }
493
+ });
494
+ }
495
+ watch(() => pagination.value.current_page, () => {
496
+ //scrola para o topo da página ao mudar de página
497
+ if (props.type_fetch === 'pagination' && props.scroll_on_trade_page) {
498
+ window.scrollTo({ top: 0, behavior: 'auto' })
499
+ }
500
+ });
501
+ if (watchSources.length > 0) {
502
+ if (props.type_fetch === 'pagination') {
503
+ watch(watchSources, () => {
504
+ pagination.value.current_page = props.page_starts_at;
505
+ fetchDataWithDelay();
506
+ }, { deep: true });
507
+ } else if (props.type_fetch === 'infinite-scroll') {
508
+ watch(watchSources, () =>{
509
+ dadosInicializados.value = false;
510
+ initDataInfinite();
511
+ } , { deep: true });
512
+ }
513
+
514
+ }
515
+
516
+ </script>
517
+
518
+ <style lang="scss" scoped>
519
+ .scroll-finish-style {
520
+ color: #6c757d;
521
+
522
+ display: block;
523
+
524
+ text-align: center;
525
+
526
+ margin-top: 1rem;
527
+ margin-bottom: 1rem;
528
+ }
529
+ </style>
@@ -0,0 +1,4 @@
1
+ import VDataPage from "./components/VDataPage.vue";
2
+ export {
3
+ VDataPage
4
+ };