v-sistec-features 1.0.0 → 1.1.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.
- package/README.md +1 -0
- package/dist/v-sistec-features.css +1 -1
- package/dist/vDataTable.js +838 -0
- package/package.json +14 -6
- package/src/DatatableVue/components/PaginationDatatable.vue +222 -0
- package/src/DatatableVue/components/SearchDatatable.vue +159 -0
- package/src/DatatableVue/components/VColumn.vue +94 -0
- package/src/DatatableVue/components/VDataTable.vue +721 -0
- package/src/DatatableVue/composables/useImagePreview.ts +61 -0
- package/src/DatatableVue/index.ts +7 -0
- package/src/DatatableVue/keys.ts +32 -0
- package/dist/vite.svg +0 -1
|
@@ -0,0 +1,721 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<div class="" :class="props.class_container">
|
|
4
|
+
<slot></slot>
|
|
5
|
+
<div class="" :class="props.class_content">
|
|
6
|
+
<div class="d-flex justify-content-between align-items-start mb-2">
|
|
7
|
+
<slot name="pageSize" :changePageSize="changePageSize" :page_size="page_size">
|
|
8
|
+
<div class="text-secondary">
|
|
9
|
+
{{ props.first_text_page_size }}
|
|
10
|
+
<div class="mx-2 d-inline-block">
|
|
11
|
+
<input class="form-control form-control-sm" @change="changePageSize" v-model="page_size" min="1" size="3"
|
|
12
|
+
aria-label="Número de nóticias por página" />
|
|
13
|
+
</div>
|
|
14
|
+
{{ props.second_text_page_size }}
|
|
15
|
+
</div>
|
|
16
|
+
</slot>
|
|
17
|
+
|
|
18
|
+
<slot name="fieldMiddle">
|
|
19
|
+
|
|
20
|
+
</slot>
|
|
21
|
+
|
|
22
|
+
<Search v-model:search="pagination.search" v-model:filter="pagination.filter" :list_filter="props.list_filter"
|
|
23
|
+
:item_use="item_use" @search="reSearch" />
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<div v-if="props.use_checkbox && selected_items.length > 0"
|
|
27
|
+
class="alert alert-cyan d-flex justify-content-center align-items-center py-3" role="alert">
|
|
28
|
+
<h4 class="alert-title m-0"> <strong>Itens Selecionados:</strong> {{ selected_items.length }}</h4>
|
|
29
|
+
<button class="btn btn-outline-danger ms-3 bold " @click="selected_items = []">Limpar Seleção</button>
|
|
30
|
+
</div>
|
|
31
|
+
<template v-if="showLoadingState">
|
|
32
|
+
<template v-if="props.custom_loading">
|
|
33
|
+
<component :is="props.custom_loading" />
|
|
34
|
+
</template>
|
|
35
|
+
<template v-else>
|
|
36
|
+
<table class="table table-vcenter table-selectable" :class="props.class_table">
|
|
37
|
+
<thead>
|
|
38
|
+
<tr>
|
|
39
|
+
<th v-for="col in columns" :key="col.field || col.header" :class="col.class_column">
|
|
40
|
+
{{ col.header }}
|
|
41
|
+
</th>
|
|
42
|
+
</tr>
|
|
43
|
+
</thead>
|
|
44
|
+
<tbody>
|
|
45
|
+
<template v-if="props.type_loading === 'placeholder'">
|
|
46
|
+
<tr v-for="n in page_size" :key="'placeholder-' + n" class="placeholder-glow">
|
|
47
|
+
<td v-for="col in columns" :key="col.field || col.header" :class="col.class_row">
|
|
48
|
+
<span v-if="col.bodySlot" >
|
|
49
|
+
<span class="placeholder col-8"></span>
|
|
50
|
+
</span>
|
|
51
|
+
<span :class="col.class_item" v-else-if="col.type === 'text'">
|
|
52
|
+
<span class="placeholder col-8"></span>
|
|
53
|
+
</span>
|
|
54
|
+
<span v-else-if="col.type === 'date'">
|
|
55
|
+
<span class="placeholder col-9"></span>
|
|
56
|
+
</span>
|
|
57
|
+
<div :class="col.class_item" v-else-if="col.type === 'html'">
|
|
58
|
+
<div class="placeholder col-12"></div>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<div :class="col.class_item" v-else-if="col.type === 'img'">
|
|
62
|
+
<div class="placeholder placeholder-img"></div>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<span class="text-danger erro-custom-container" v-else>tipo <span
|
|
66
|
+
class="badge bg-orange text-white erro-custom-text">{{ col.type }}</span> não suportado
|
|
67
|
+
</span>
|
|
68
|
+
</td>
|
|
69
|
+
</tr>
|
|
70
|
+
</template>
|
|
71
|
+
<template v-else-if="props.type_loading === 'spiner-table'">
|
|
72
|
+
<tr v-for="n in page_size" :key="'placeholder-' + n">
|
|
73
|
+
<td v-for="col in columns" :key="col.field || col.header" :class="col.class_row">
|
|
74
|
+
<span v-if="col.bodySlot" >
|
|
75
|
+
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
|
76
|
+
</span>
|
|
77
|
+
<span :class="col.class_item" v-else-if="col.type === 'text'">
|
|
78
|
+
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
|
79
|
+
</span>
|
|
80
|
+
<span v-else-if="col.type === 'date'">
|
|
81
|
+
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
|
82
|
+
</span>
|
|
83
|
+
<div :class="col.class_item" v-else-if="col.type === 'html'">
|
|
84
|
+
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<div class="" :class="col.class_item" v-else-if="col.type === 'img'">
|
|
88
|
+
<span class="placeholder-img d-flex justify-content-center align-items-center">
|
|
89
|
+
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
|
90
|
+
</span>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<span class="text-danger erro-custom-container" v-else>tipo <span
|
|
94
|
+
class="badge bg-orange text-white erro-custom-text">{{ col.type }}</span> não suportado
|
|
95
|
+
</span>
|
|
96
|
+
</td>
|
|
97
|
+
</tr>
|
|
98
|
+
|
|
99
|
+
</template>
|
|
100
|
+
<template v-else-if="props.type_loading === 'spiner'">
|
|
101
|
+
<tr v-for="n in page_size" :key="n">
|
|
102
|
+
<td :colspan="columns.length" class="text-center p-0" style="border-bottom: none;">
|
|
103
|
+
<div v-if="n === Math.floor(page_size / 2) + 1"
|
|
104
|
+
class="d-flex flex-column justify-content-center align-items-center" style="height: 6rem;">
|
|
105
|
+
<div class="spinner-border" style="width: 3rem; height: 3rem;" role="status">
|
|
106
|
+
</div>
|
|
107
|
+
<span class="mt-2">Carregando...</span>
|
|
108
|
+
</div>
|
|
109
|
+
<div v-else style="height: 3rem;"></div>
|
|
110
|
+
</td>
|
|
111
|
+
</tr>
|
|
112
|
+
</template>
|
|
113
|
+
|
|
114
|
+
</tbody>
|
|
115
|
+
</table>
|
|
116
|
+
</template>
|
|
117
|
+
|
|
118
|
+
<div v-if="attempt && attempt.current > 1" class="p-3 text-center text-secondary">
|
|
119
|
+
A conexão falhou. Tentando novamente... (Tentativa {{ attempt.current }} de {{ attempt.total }})
|
|
120
|
+
</div>
|
|
121
|
+
</template>
|
|
122
|
+
<div v-else-if="error" class="feedback-container text-center">
|
|
123
|
+
<h4 class="text-danger">Ocorreu um Erro</h4>
|
|
124
|
+
<p class="text-secondary" v-if="attempt">
|
|
125
|
+
Não foi possível carregar os dados após {{ attempt.total }} tentativa(s).
|
|
126
|
+
</p>
|
|
127
|
+
<p class="text-secondary" v-else>
|
|
128
|
+
Não foi possível carregar os dados. Verifique sua conexão.
|
|
129
|
+
</p>
|
|
130
|
+
<button class="btn btn-primary mt-2" @click="fetchDataWithDelay">
|
|
131
|
+
Tentar Novamente
|
|
132
|
+
</button>
|
|
133
|
+
</div>
|
|
134
|
+
<div class="table-responsive" v-else-if="items">
|
|
135
|
+
<div v-if="items.length > 0">
|
|
136
|
+
<table class="table table-vcenter table-selectable" :class="props.class_table">
|
|
137
|
+
<thead>
|
|
138
|
+
<tr>
|
|
139
|
+
<th v-if="props.use_checkbox" class="w-1">
|
|
140
|
+
<input class="form-check-input m-0" type="checkbox" ref="selectAllCheckbox"
|
|
141
|
+
@change="toggleSelectAll" aria-label="Selecionar todos os itens na página" />
|
|
142
|
+
</th>
|
|
143
|
+
<th v-for="col in columns" :key="col.field || col.header" :class="col.class_column">
|
|
144
|
+
{{ col.header }}
|
|
145
|
+
</th>
|
|
146
|
+
</tr>
|
|
147
|
+
</thead>
|
|
148
|
+
<tbody>
|
|
149
|
+
<tr v-for="item in items" :key="item[props.item_key]">
|
|
150
|
+
<td v-if="props.use_checkbox" class="w-1">
|
|
151
|
+
<input class="form-check-input m-0" type="checkbox" :checked="isSelected(item)"
|
|
152
|
+
@change="toggleItemSelection(item)" aria-label="Selecionar este item" />
|
|
153
|
+
</td>
|
|
154
|
+
<td v-for="col in columns" :key="col.field || col.header" :class="col.class_row">
|
|
155
|
+
<component v-if="col.bodySlot" :is="col.bodySlot" :item="item" :is-selected="isSelected(item)" />
|
|
156
|
+
<span :class="col.class_item" v-else-if="col.type === 'text'">
|
|
157
|
+
{{
|
|
158
|
+
limiteText(getSubItem(col.field, item, col.transform_function), col.limite_text ?? null)
|
|
159
|
+
}}</span>
|
|
160
|
+
|
|
161
|
+
<span v-else-if="col.type === 'date'">
|
|
162
|
+
<span v-if="col.format === 'complete'">{{ new Date(getSubItem(col.field, item)).toLocaleString()
|
|
163
|
+
}}</span>
|
|
164
|
+
<span v-if="col.format === 'simple'"> {{ new Date(getSubItem(col.field,
|
|
165
|
+
item)).toLocaleDateString()
|
|
166
|
+
}} </span>
|
|
167
|
+
</span>
|
|
168
|
+
<div :class="col.class_item" v-else-if="col.type === 'html'" v-html="getSubItem(col.field, item)">
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
<div :class="col.class_item" v-else-if="col.type === 'img'">
|
|
172
|
+
|
|
173
|
+
<div v-if="getSubItem(col.field, item)" v-bind="col.deactivate_img_preview ? {
|
|
174
|
+
class: 'container-img'
|
|
175
|
+
} :
|
|
176
|
+
{
|
|
177
|
+
onMouseover: (event) => handleMouseOver(event, getSubItem(col.field, item)),
|
|
178
|
+
onMousemove: handleMouseMove,
|
|
179
|
+
onMouseleave: handleMouseLeave,
|
|
180
|
+
class: 'container-img container-img-preview'
|
|
181
|
+
}">
|
|
182
|
+
|
|
183
|
+
<img class="img-tamanho" :src="getSubItem(col.field, item)" />
|
|
184
|
+
<img class="img-tamanho-cover" :src="getSubItem(col.field, item)" />
|
|
185
|
+
<div class="bg-img"></div>
|
|
186
|
+
</div>
|
|
187
|
+
|
|
188
|
+
</div>
|
|
189
|
+
<span class="text-danger erro-custom-container" v-else>tipo <span
|
|
190
|
+
class="badge bg-orange text-white erro-custom-text">{{ col.type }}</span> não suportado</span>
|
|
191
|
+
</td>
|
|
192
|
+
</tr>
|
|
193
|
+
</tbody>
|
|
194
|
+
</table>
|
|
195
|
+
</div>
|
|
196
|
+
<div v-else class="text-center p-4 text-secondary">
|
|
197
|
+
<p class="m-0">Nenhum item encontrado.</p>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
|
|
201
|
+
</div>
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
</div>
|
|
205
|
+
<slot name="pagination" :pagination="pagination" :tradePage="fetchDataWithDelay" :error="error">
|
|
206
|
+
<div v-if="!error && pagination.count > 0" class="mt-3 px-3" :class="props.class_pagination">
|
|
207
|
+
<PaginationDatatable :filtering="true" :pagination="pagination" @tradePage="fetchDataWithDelay" />
|
|
208
|
+
</div>
|
|
209
|
+
</slot>
|
|
210
|
+
|
|
211
|
+
<div v-if="isHovering" class="image-preview-container" :style="previewStyle">
|
|
212
|
+
<img :src="previewSrc" alt="Preview" class="image-preview-large" />
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
|
|
216
|
+
</template>
|
|
217
|
+
|
|
218
|
+
<script setup lang="ts" generic="T extends Record<string, any>" >
|
|
219
|
+
import { ref, provide, computed, watch, onMounted, nextTick, type Component, type Ref, type ComputedRef } from 'vue';
|
|
220
|
+
|
|
221
|
+
import PaginationDatatable from './PaginationDatatable.vue';
|
|
222
|
+
import Search from './SearchDatatable.vue';
|
|
223
|
+
import { useImagePreview } from '../composables/useImagePreview';
|
|
224
|
+
import { dataTableApiKey, type ColumnConfiguration, type PaginationObject } from '../keys';
|
|
225
|
+
|
|
226
|
+
const {
|
|
227
|
+
isHovering,
|
|
228
|
+
previewSrc,
|
|
229
|
+
previewStyle,
|
|
230
|
+
handleMouseOver,
|
|
231
|
+
handleMouseMove,
|
|
232
|
+
handleMouseLeave
|
|
233
|
+
} = useImagePreview();
|
|
234
|
+
|
|
235
|
+
interface VDataTableProps {
|
|
236
|
+
/* configuração do useApiFetch */
|
|
237
|
+
fetch: Function;
|
|
238
|
+
fetch_name?: string;
|
|
239
|
+
endpoint: string;
|
|
240
|
+
/* tipos de loading pré-definidos*/
|
|
241
|
+
type_loading?: 'placeholder' | 'spiner-table' | 'spiner';
|
|
242
|
+
/*recebe um component para loading*/
|
|
243
|
+
custom_loading?: Component | null;
|
|
244
|
+
/* retira os params default da requisição */
|
|
245
|
+
deactivate_default_params?: boolean;
|
|
246
|
+
/* nomes dos parâmetros para passar para o backend */
|
|
247
|
+
filter_param_name?: string;
|
|
248
|
+
search_param_name?: string;
|
|
249
|
+
page_param_name?: string;
|
|
250
|
+
page_size_param_name?: string;
|
|
251
|
+
add_params?: Object | Function;
|
|
252
|
+
|
|
253
|
+
/* usado para pegar os dados do useApiFetch */
|
|
254
|
+
data_key?: string;
|
|
255
|
+
total_key?: string;
|
|
256
|
+
|
|
257
|
+
/* filtros que irão ser usados */
|
|
258
|
+
list_filter?: any[];
|
|
259
|
+
/* mudar o que está escrito no select de mudança de items_per_page*/
|
|
260
|
+
first_text_page_size?: string;
|
|
261
|
+
second_text_page_size?: string;
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
/* props para estilizar o vdatatable */
|
|
265
|
+
class_table?: string;
|
|
266
|
+
class_content?: string;
|
|
267
|
+
class_container?: string;
|
|
268
|
+
class_pagination?: string;
|
|
269
|
+
|
|
270
|
+
/*
|
|
271
|
+
* tempo mínimo em ms para mostrar o loading para evitar telas piscando
|
|
272
|
+
*/
|
|
273
|
+
min_loading_delay?: number;
|
|
274
|
+
/*
|
|
275
|
+
- Número de tentativas automáticas em caso de falha.
|
|
276
|
+
- 1 significa que a requisição será feita apenas uma vez, sem retentativas.
|
|
277
|
+
- Valor padrão é 3.
|
|
278
|
+
*/
|
|
279
|
+
retry_attempts?: number;
|
|
280
|
+
// Atraso em milissegundos entre cada tentativa
|
|
281
|
+
retry_delay?: number;
|
|
282
|
+
|
|
283
|
+
// Ativa a funcionalidade de seleção com checkboxes
|
|
284
|
+
use_checkbox?: boolean;
|
|
285
|
+
// Define qual propriedade do item será usada como chave única para a seleção.
|
|
286
|
+
item_key?: string;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
interface ExposedFunctions {
|
|
290
|
+
execute: () => void;
|
|
291
|
+
pagination: Ref<PaginationObject>;
|
|
292
|
+
default_params: Record<string, any>;
|
|
293
|
+
selected_items: Ref<T[]>;
|
|
294
|
+
atLeastOneSelected: ComputedRef<boolean>;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// =======================================================
|
|
298
|
+
// 1. DEFINIÇÃO DE PROPS COM VALORES PADRÃO
|
|
299
|
+
// =======================================================
|
|
300
|
+
const props = withDefaults(defineProps<VDataTableProps>(), {
|
|
301
|
+
fetch_name: '',
|
|
302
|
+
type_loading: 'placeholder',
|
|
303
|
+
custom_loading: null,
|
|
304
|
+
deactivate_default_params: false,
|
|
305
|
+
filter_param_name: 'filter',
|
|
306
|
+
search_param_name: 'search',
|
|
307
|
+
page_param_name: 'page',
|
|
308
|
+
page_size_param_name: 'page_size',
|
|
309
|
+
add_params: () => ({}),
|
|
310
|
+
data_key: 'results',
|
|
311
|
+
total_key: 'count',
|
|
312
|
+
list_filter: () => [],
|
|
313
|
+
class_table: '',
|
|
314
|
+
class_content: '',
|
|
315
|
+
class_container: '',
|
|
316
|
+
class_pagination: '',
|
|
317
|
+
min_loading_delay: 600,
|
|
318
|
+
retry_attempts: 3,
|
|
319
|
+
retry_delay: 2000,
|
|
320
|
+
use_checkbox: false,
|
|
321
|
+
item_key: 'id',
|
|
322
|
+
first_text_page_size: 'Mostrar',
|
|
323
|
+
second_text_page_size: 'registros',
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
// =======================================================
|
|
328
|
+
// 2. ESTADO REATIVO PRINCIPAL
|
|
329
|
+
// =======================================================
|
|
330
|
+
|
|
331
|
+
const page_size = ref<number>(5);
|
|
332
|
+
const columns = ref<ColumnConfiguration[]>([]);
|
|
333
|
+
const items = ref<T[]>([]) as Ref<T[]>;
|
|
334
|
+
const totalItems = ref<number>(0);
|
|
335
|
+
const selected_items = ref<T[]>([]) as Ref<T[]>;
|
|
336
|
+
const selectAllCheckbox = ref<HTMLInputElement | null>(null);
|
|
337
|
+
const isDelaying = ref<boolean>(false);
|
|
338
|
+
const delayTimer = ref<ReturnType<typeof setTimeout> | null>(null);
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
/*--------- definição de páginação ---------------*/
|
|
342
|
+
const pagination = ref<PaginationObject>({
|
|
343
|
+
current_page: 0, // pagina atual
|
|
344
|
+
count: 0, // total de itens
|
|
345
|
+
limit_per_page: 5, // limite de itens por página
|
|
346
|
+
search: '', // termo de busca
|
|
347
|
+
filter: '', // filtro selecionado
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
// =======================================================
|
|
351
|
+
// 3. LÓGICA DA API (useFetch)
|
|
352
|
+
// =======================================================
|
|
353
|
+
const { data: response, pending, error, execute, attempt } = props.fetch(props.endpoint, {
|
|
354
|
+
params: () => {
|
|
355
|
+
|
|
356
|
+
if (props.deactivate_default_params) {
|
|
357
|
+
if (props.add_params && typeof props.add_params === 'function') {
|
|
358
|
+
return props.add_params();
|
|
359
|
+
}
|
|
360
|
+
return {
|
|
361
|
+
...props.add_params,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
else if (props.add_params && typeof props.add_params === 'function') {
|
|
365
|
+
return {
|
|
366
|
+
...default_params.value,
|
|
367
|
+
...props.add_params(),
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return {
|
|
371
|
+
...default_params.value,
|
|
372
|
+
...props.add_params,
|
|
373
|
+
};
|
|
374
|
+
},
|
|
375
|
+
retry: props.retry_attempts,
|
|
376
|
+
retryDelay: props.retry_delay,
|
|
377
|
+
paramsReactives: false,
|
|
378
|
+
immediate: false,
|
|
379
|
+
}, props.fetch_name);
|
|
380
|
+
|
|
381
|
+
// =======================================================
|
|
382
|
+
// 4. PROPRIEDADES COMPUTADAS
|
|
383
|
+
// =======================================================
|
|
384
|
+
const item_use = computed<number[]>(() => {
|
|
385
|
+
let use = [1]
|
|
386
|
+
if (props.list_filter.length > 0) {
|
|
387
|
+
use.push(2)
|
|
388
|
+
}
|
|
389
|
+
return use;
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
const default_params = computed<Record<string, any>>(() => ({
|
|
393
|
+
[props.page_param_name]: pagination.value.current_page + 1,
|
|
394
|
+
[props.page_size_param_name]: pagination.value.limit_per_page,
|
|
395
|
+
[props.search_param_name]: pagination.value.search || "",
|
|
396
|
+
[props.filter_param_name]: pagination.value.filter || "",
|
|
397
|
+
}));
|
|
398
|
+
|
|
399
|
+
// para controlar a exibição do loading
|
|
400
|
+
const showLoadingState = computed<boolean>(() => {
|
|
401
|
+
return (pending.value || isDelaying.value)
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
// Helper para verificar se um item está selecionado, comparando pela chave única
|
|
406
|
+
const isSelected = (item: T): boolean => {
|
|
407
|
+
const key = props.item_key;
|
|
408
|
+
return selected_items.value.some(selectedItem => selectedItem[key] === item[key]);
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
// Controla o estado do checkbox "selecionar todos"
|
|
412
|
+
const selectAllState = computed<boolean | 'indeterminate'>(() => {
|
|
413
|
+
if (!items.value.length) return false;
|
|
414
|
+
const selectedOnPageCount = items.value.filter(item => isSelected(item)).length;
|
|
415
|
+
if (selectedOnPageCount === 0) return false;
|
|
416
|
+
if (selectedOnPageCount === items.value.length) return true;
|
|
417
|
+
return 'indeterminate';
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
// computed que mostra se pelo menos um item está selecionado
|
|
421
|
+
const atLeastOneSelected = computed<boolean>(() => selected_items.value.length > 0);
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
// =======================================================
|
|
425
|
+
// 5. WATCHERS (Observadores)
|
|
426
|
+
// =======================================================
|
|
427
|
+
|
|
428
|
+
// observa o estado e atualiza a propriedade 'indeterminate'
|
|
429
|
+
watch([selectAllState, selectAllCheckbox], ([newState]) => {
|
|
430
|
+
if (selectAllCheckbox.value) {
|
|
431
|
+
if (newState === 'indeterminate') {
|
|
432
|
+
console.log("entrei no indeterminate")
|
|
433
|
+
// Se o estado for indeterminado:
|
|
434
|
+
selectAllCheckbox.value.checked = false; // Ele não está "marcado"
|
|
435
|
+
selectAllCheckbox.value.indeterminate = true; // Ele está com o "traço"
|
|
436
|
+
} else {
|
|
437
|
+
selectAllCheckbox.value.checked = newState; // Define o estado marcado/desmarcado
|
|
438
|
+
selectAllCheckbox.value.indeterminate = false; // Remove o "traço"
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}, {
|
|
442
|
+
immediate: true,
|
|
443
|
+
flush: 'post'
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
watch(response, (newResponse: any) => {
|
|
447
|
+
if (newResponse) {
|
|
448
|
+
items.value = newResponse[props.data_key] || [];
|
|
449
|
+
totalItems.value = newResponse[props.total_key] || 0;
|
|
450
|
+
pagination.value.count = totalItems.value;
|
|
451
|
+
} else {
|
|
452
|
+
items.value = [];
|
|
453
|
+
totalItems.value = 0;
|
|
454
|
+
}
|
|
455
|
+
}, { immediate: true });
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
// =======================================================
|
|
459
|
+
// 6. MÉTODOS
|
|
460
|
+
// =======================================================
|
|
461
|
+
|
|
462
|
+
// Função para marcar ou desmarcar todos os itens da página atual
|
|
463
|
+
function toggleSelectAll(): void {
|
|
464
|
+
const pageItems = items.value;
|
|
465
|
+
if (!pageItems.length) return;
|
|
466
|
+
|
|
467
|
+
// Usa a propriedade computada para saber o estado atual
|
|
468
|
+
const currentState = selectAllState.value;
|
|
469
|
+
|
|
470
|
+
// Se TODOS ou ALGUNS estiverem selecionados, o clique irá LIMPAR a seleção da página.
|
|
471
|
+
if (currentState === true || currentState === 'indeterminate') {
|
|
472
|
+
const pageItemKeys = pageItems.map(item => item[props.item_key]);
|
|
473
|
+
selected_items.value = selected_items.value.filter(
|
|
474
|
+
selectedItem => !pageItemKeys.includes(selectedItem[props.item_key])
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
// Se NENHUM estiver selecionado, o clique irá SELECIONAR TODOS da página.
|
|
478
|
+
else { // currentState é false
|
|
479
|
+
pageItems.forEach(item => {
|
|
480
|
+
if (!isSelected(item)) {
|
|
481
|
+
selected_items.value.push(item);
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Função para marcar ou desmarcar um item individual
|
|
488
|
+
function toggleItemSelection(item: T): void {
|
|
489
|
+
const key = props.item_key;
|
|
490
|
+
const index = selected_items.value.findIndex(selectedItem => selectedItem[key] === item[key]);
|
|
491
|
+
|
|
492
|
+
if (index > -1) {
|
|
493
|
+
selected_items.value.splice(index, 1); // Remove se já existe
|
|
494
|
+
} else {
|
|
495
|
+
selected_items.value.push(item); // Adiciona se não existe
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function addColumn(colConfig: ColumnConfiguration): void {
|
|
500
|
+
columns.value.push(colConfig);
|
|
501
|
+
}
|
|
502
|
+
provide(dataTableApiKey, { addColumn });
|
|
503
|
+
|
|
504
|
+
// Função que gerencia o delay e a chamada da API
|
|
505
|
+
function fetchDataWithDelay(): void {
|
|
506
|
+
// Limpa timer anterior, se houver
|
|
507
|
+
if (delayTimer.value) clearTimeout(delayTimer.value);
|
|
508
|
+
|
|
509
|
+
isDelaying.value = true;
|
|
510
|
+
|
|
511
|
+
delayTimer.value = setTimeout(() => {
|
|
512
|
+
isDelaying.value = false;
|
|
513
|
+
}, props.min_loading_delay);
|
|
514
|
+
|
|
515
|
+
execute(); // Executa a busca de dados original do useApiFetch
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function reSearch(): void {
|
|
519
|
+
pagination.value.current_page = 0;
|
|
520
|
+
fetchDataWithDelay();
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const changePageSize = (event: Event): void => {
|
|
524
|
+
const target = event.target as HTMLInputElement;
|
|
525
|
+
const newSize = parseInt(target.value, 10);
|
|
526
|
+
if (newSize > 0) {
|
|
527
|
+
page_size.value = newSize;
|
|
528
|
+
pagination.value.limit_per_page = newSize; // Atualiza o limite de itens por página
|
|
529
|
+
pagination.value.current_page = 0;
|
|
530
|
+
fetchDataWithDelay();
|
|
531
|
+
} else {
|
|
532
|
+
// toast.showToast("Erro", "Tamanho da página deve ser maior que 0", 2);
|
|
533
|
+
page_size.value = pagination.value.limit_per_page; // Reseta para o valor anterior
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
function getSubItem(field: string | null, item: T, transform_function: ((value: any) => any) | null = null): any {
|
|
538
|
+
if (!field) return item;
|
|
539
|
+
const parts = field.split('.');
|
|
540
|
+
let value_item = item;
|
|
541
|
+
|
|
542
|
+
for(const part of parts){
|
|
543
|
+
if (value_item && typeof value_item === 'object' && part in value_item){
|
|
544
|
+
value_item = value_item[part];
|
|
545
|
+
}
|
|
546
|
+
else{
|
|
547
|
+
console.error(`Caminho inválido ou valor nulo em: ${field} na parte ${part}`);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (transform_function) {
|
|
552
|
+
value_item = transform_function(value_item);
|
|
553
|
+
}
|
|
554
|
+
return value_item;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
function limiteText(text: string | null, limite: number | null): string | null {
|
|
560
|
+
if (limite && typeof limite === 'number' && limite > 0 && typeof text === 'string' && text.length > limite) {
|
|
561
|
+
return text.substring(0, limite) + '...';
|
|
562
|
+
}
|
|
563
|
+
return text;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// =======================================================
|
|
567
|
+
// 7. EXPOSE E CICLO DE VIDA
|
|
568
|
+
// =======================================================
|
|
569
|
+
|
|
570
|
+
defineExpose<
|
|
571
|
+
ExposedFunctions
|
|
572
|
+
>({
|
|
573
|
+
execute: fetchDataWithDelay,
|
|
574
|
+
pagination,
|
|
575
|
+
default_params,
|
|
576
|
+
selected_items,
|
|
577
|
+
atLeastOneSelected,
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
onMounted(() => {
|
|
581
|
+
nextTick(() => {
|
|
582
|
+
|
|
583
|
+
/*
|
|
584
|
+
* executar dentro do nextTick para garantir que o pai já tem acesso ao
|
|
585
|
+
* ref que foi exposto
|
|
586
|
+
*/
|
|
587
|
+
fetchDataWithDelay();
|
|
588
|
+
})
|
|
589
|
+
});
|
|
590
|
+
</script>
|
|
591
|
+
|
|
592
|
+
<style lang="scss" scoped>
|
|
593
|
+
.table-responsive {
|
|
594
|
+
overflow-x: auto;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
.state-feedback {
|
|
598
|
+
padding: 1rem;
|
|
599
|
+
text-align: center;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
.state-feedback.error {
|
|
603
|
+
color: red;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
.pagination-controls {
|
|
607
|
+
margin-top: 1rem;
|
|
608
|
+
display: flex;
|
|
609
|
+
justify-content: space-between;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
$max-width-img: 40px;
|
|
613
|
+
|
|
614
|
+
.placeholder-img {
|
|
615
|
+
width: $max-width-img;
|
|
616
|
+
height: $max-width-img;
|
|
617
|
+
border-radius: 4px;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
.container-img {
|
|
621
|
+
aspect-ratio: 1;
|
|
622
|
+
display: flex;
|
|
623
|
+
justify-content: center;
|
|
624
|
+
overflow: hidden;
|
|
625
|
+
position: relative;
|
|
626
|
+
max-width: $max-width-img;
|
|
627
|
+
min-width: $max-width-img;
|
|
628
|
+
|
|
629
|
+
.img-tamanho-cover {
|
|
630
|
+
position: absolute;
|
|
631
|
+
top: 0;
|
|
632
|
+
width: 100%;
|
|
633
|
+
height: 100%;
|
|
634
|
+
object-fit: cover;
|
|
635
|
+
z-index: 0;
|
|
636
|
+
opacity: 0.5;
|
|
637
|
+
filter: blur(4px);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
.img-tamanho {
|
|
641
|
+
object-fit: contain;
|
|
642
|
+
width: 100%;
|
|
643
|
+
height: 100%;
|
|
644
|
+
z-index: 2;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
&.container-img-preview {
|
|
648
|
+
cursor: pointer;
|
|
649
|
+
|
|
650
|
+
&:hover {
|
|
651
|
+
border-style: dashed;
|
|
652
|
+
border-color: var(--tblr-primary);
|
|
653
|
+
border-width: 2px;
|
|
654
|
+
transition: border-width 0.15s ease-in-out;
|
|
655
|
+
|
|
656
|
+
.img-tamanho {
|
|
657
|
+
opacity: 0.3;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
.erro-custom-container {
|
|
664
|
+
display: inline-block;
|
|
665
|
+
padding: 0.2em 0.6em;
|
|
666
|
+
border-radius: 4px;
|
|
667
|
+
background-color: #ffffff;
|
|
668
|
+
font-weight: bold;
|
|
669
|
+
text-transform: uppercase;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
.erro-custom-text {
|
|
673
|
+
font-size: 0.8em;
|
|
674
|
+
text-transform: uppercase;
|
|
675
|
+
font-weight: bold;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
.bg-img {
|
|
679
|
+
background-color: #0000004d;
|
|
680
|
+
position: absolute;
|
|
681
|
+
top: 0;
|
|
682
|
+
width: 100%;
|
|
683
|
+
height: 100%;
|
|
684
|
+
z-index: 1;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
$max-width-preview: 250px;
|
|
688
|
+
|
|
689
|
+
.image-preview-container {
|
|
690
|
+
position: fixed;
|
|
691
|
+
|
|
692
|
+
z-index: 9999;
|
|
693
|
+
|
|
694
|
+
background-color: #fff;
|
|
695
|
+
border-radius: 8px;
|
|
696
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
|
697
|
+
padding: 5px;
|
|
698
|
+
|
|
699
|
+
pointer-events: none;
|
|
700
|
+
transition: opacity 0.2s ease-in-out;
|
|
701
|
+
|
|
702
|
+
.image-preview-large {
|
|
703
|
+
display: block;
|
|
704
|
+
max-width: $max-width-preview;
|
|
705
|
+
|
|
706
|
+
max-height: $max-width-preview;
|
|
707
|
+
border-radius: 4px;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
.form-check-input {
|
|
712
|
+
border-width: 1px !important;
|
|
713
|
+
border-color: rgba(0, 0, 0, 0.391) !important;
|
|
714
|
+
width: 17px;
|
|
715
|
+
height: 17px;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
[data-bs-theme=dark] .form-check-input {
|
|
719
|
+
border-color: rgba(255, 255, 255, 0.374) !important;
|
|
720
|
+
}
|
|
721
|
+
</style>
|