da-table-di 1.0.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.
- package/README.md +78 -0
- package/index.js +11 -0
- package/package.json +31 -0
- package/src/BaseTable.vue +167 -0
package/README.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# da-table (BaseTable)
|
|
2
|
+
|
|
3
|
+
Componente reutilizable de **tabla Quasar (QTable)** para Vue 3.
|
|
4
|
+
Pensado para el 80% de casos típicos: paginación, carga desde API y acciones comunes (ver/editar/eliminar/etc).
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Requisitos
|
|
9
|
+
|
|
10
|
+
- **Vue 3**
|
|
11
|
+
- **Quasar v2** (el proyecto padre debe tener Quasar configurado)
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Instalación
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm i da-table
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Importación
|
|
22
|
+
|
|
23
|
+
```vue
|
|
24
|
+
<template>
|
|
25
|
+
<div class="q-pa-md">
|
|
26
|
+
<BaseTable
|
|
27
|
+
:columns="columns"
|
|
28
|
+
:loadAction="loadData"
|
|
29
|
+
selection="multiple"
|
|
30
|
+
:editAction="onEdit"
|
|
31
|
+
:deleteAction="onDelete"
|
|
32
|
+
class="dt-table"
|
|
33
|
+
/>
|
|
34
|
+
</div>
|
|
35
|
+
</template>
|
|
36
|
+
|
|
37
|
+
<script setup>
|
|
38
|
+
import { BaseTable } from 'da-table'
|
|
39
|
+
|
|
40
|
+
const columns = [
|
|
41
|
+
{ name: 'name', label: 'Nombre', field: 'name', align: 'left', sortable: true },
|
|
42
|
+
{ name: 'age', label: 'Edad', field: 'age', align: 'left', sortable: true }
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
// La tabla llama a loadAction pasando paginación/sort
|
|
46
|
+
// Debe devolver:
|
|
47
|
+
// A) { rows: Array, total: number } (recomendado)
|
|
48
|
+
// B) Array (si no tienes total)
|
|
49
|
+
const loadData = async ({ page, rowsPerPage, sortBy, descending }) => {
|
|
50
|
+
// Ejemplo:
|
|
51
|
+
return {
|
|
52
|
+
rows: [
|
|
53
|
+
{ id: 1, name: 'José', age: 35 },
|
|
54
|
+
{ id: 2, name: 'Ana', age: 28 }
|
|
55
|
+
],
|
|
56
|
+
total: 2
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const onEdit = ({ row }) => console.log('edit', row)
|
|
61
|
+
const onDelete = ({ row }) => console.log('delete', row)
|
|
62
|
+
</script>
|
|
63
|
+
|
|
64
|
+
<style scoped>
|
|
65
|
+
/* Estilos desde el proyecto padre (scoped) usando :deep */
|
|
66
|
+
:deep(.dt-table .q-table__container) {
|
|
67
|
+
border-radius: 16px;
|
|
68
|
+
overflow: hidden;
|
|
69
|
+
border: 1px solid rgba(0, 0, 0, 0.12);
|
|
70
|
+
background: white;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
:deep(.dt-table .q-table__top),
|
|
74
|
+
:deep(.dt-table .q-table__bottom) {
|
|
75
|
+
background: #f6f7f9 !important;
|
|
76
|
+
}
|
|
77
|
+
</style>
|
|
78
|
+
```
|
package/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "da-table-di",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"main": "index.js",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"files": [
|
|
7
|
+
"src",
|
|
8
|
+
"index.js"
|
|
9
|
+
],
|
|
10
|
+
"peerDependencies": {
|
|
11
|
+
"quasar": "^2.0.0",
|
|
12
|
+
"vue": "^3.0.0"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"vue",
|
|
16
|
+
"quasar",
|
|
17
|
+
"table",
|
|
18
|
+
"component"
|
|
19
|
+
],
|
|
20
|
+
"author": "Cristina Bermudez",
|
|
21
|
+
"license": "ISC",
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@vitejs/plugin-vue": "^6.0.4",
|
|
24
|
+
"quasar": "^2.18.6",
|
|
25
|
+
"vite": "^7.3.1",
|
|
26
|
+
"vue": "^3.5.28"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "vite build"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<q-table
|
|
4
|
+
:rows="rows"
|
|
5
|
+
:columns="computedColumns"
|
|
6
|
+
:row-key="rowKey"
|
|
7
|
+
:loading="loading"
|
|
8
|
+
:pagination="pagination"
|
|
9
|
+
:rows-per-page-options="rowsPerPageOptions"
|
|
10
|
+
:no-data-label="noDataLabel"
|
|
11
|
+
:loading-label="loadingLabel"
|
|
12
|
+
:rows-per-page-label="rowsPerPageLabel"
|
|
13
|
+
flat
|
|
14
|
+
:selection="selection"
|
|
15
|
+
v-model:selected="selected"
|
|
16
|
+
@request="onRequest"
|
|
17
|
+
class="dt-table"
|
|
18
|
+
>
|
|
19
|
+
<!-- Columna de acciones -->
|
|
20
|
+
<template v-slot:body-cell-actions="props">
|
|
21
|
+
<q-td :props="props" class="q-gutter-x-xs">
|
|
22
|
+
<q-btn v-if="editAction" flat round icon="edit" title="Editar" @click="editAction(props)" />
|
|
23
|
+
<q-btn v-if="deleteAction" flat round icon="delete" title="Eliminar" @click="deleteAction(props)" />
|
|
24
|
+
<q-btn v-if="viewAction" flat round icon="visibility" title="Ver" @click="viewAction(props)" />
|
|
25
|
+
<q-btn v-if="otherAction" flat round icon="key" title="Acción" @click="otherAction(props)" />
|
|
26
|
+
<q-btn v-if="reportAction" flat round icon="description" title="Generar informe" @click="reportAction(props)" />
|
|
27
|
+
<q-btn v-if="downloadAction" flat round icon="download" title="Descargar" @click="downloadAction(props)" />
|
|
28
|
+
</q-td>
|
|
29
|
+
</template>
|
|
30
|
+
|
|
31
|
+
<!-- Punto de personalización futuro: se pueden añadir slots genéricos -->
|
|
32
|
+
<template v-if="$slots.top" #top>
|
|
33
|
+
<slot name="top" />
|
|
34
|
+
</template>
|
|
35
|
+
</q-table>
|
|
36
|
+
</div>
|
|
37
|
+
</template>
|
|
38
|
+
|
|
39
|
+
<script setup>
|
|
40
|
+
import { computed, onMounted, ref } from 'vue'
|
|
41
|
+
|
|
42
|
+
const props = defineProps({
|
|
43
|
+
// tabla
|
|
44
|
+
columns: { type: Array, required: true },
|
|
45
|
+
rowKey: { type: String, default: 'id' },
|
|
46
|
+
|
|
47
|
+
// carga/paginación
|
|
48
|
+
loadAction: { type: Function, required: true },
|
|
49
|
+
initialPagination: {
|
|
50
|
+
type: Object,
|
|
51
|
+
default: () => ({
|
|
52
|
+
page: 1,
|
|
53
|
+
rowsPerPage: 10,
|
|
54
|
+
rowsNumber: 0,
|
|
55
|
+
sortBy: null,
|
|
56
|
+
descending: false
|
|
57
|
+
})
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
// textos
|
|
61
|
+
noDataLabel: { type: String, default: 'No hay datos' },
|
|
62
|
+
loadingLabel: { type: String, default: 'Cargando datos...' },
|
|
63
|
+
rowsPerPageLabel: { type: String, default: 'Registros por página' },
|
|
64
|
+
rowsPerPageOptions: { type: Array, default: () => [10, 20, 40, 60] },
|
|
65
|
+
|
|
66
|
+
// selección
|
|
67
|
+
selection: { type: String, default: 'none' }, // none | single | multiple
|
|
68
|
+
|
|
69
|
+
// acciones (opcionales)
|
|
70
|
+
editAction: { type: Function, default: null },
|
|
71
|
+
deleteAction: { type: Function, default: null },
|
|
72
|
+
viewAction: { type: Function, default: null },
|
|
73
|
+
otherAction: { type: Function, default: null },
|
|
74
|
+
reportAction: { type: Function, default: null },
|
|
75
|
+
downloadAction: { type: Function, default: null },
|
|
76
|
+
|
|
77
|
+
// si quieres forzar columna actions si hay acciones
|
|
78
|
+
ensureActionsColumn: { type: Boolean, default: true }
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
const emit = defineEmits(['loaded', 'error'])
|
|
82
|
+
|
|
83
|
+
const rows = ref([])
|
|
84
|
+
const loading = ref(false)
|
|
85
|
+
const selected = ref([])
|
|
86
|
+
const pagination = ref({ ...props.initialPagination })
|
|
87
|
+
|
|
88
|
+
const hasAnyAction = computed(() =>
|
|
89
|
+
!!(
|
|
90
|
+
props.editAction ||
|
|
91
|
+
props.deleteAction ||
|
|
92
|
+
props.viewAction ||
|
|
93
|
+
props.otherAction ||
|
|
94
|
+
props.reportAction ||
|
|
95
|
+
props.downloadAction
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
const computedColumns = computed(() => {
|
|
100
|
+
const cols = Array.isArray(props.columns) ? [...props.columns] : []
|
|
101
|
+
const hasActionsCol = cols.some(c => c && c.name === 'actions')
|
|
102
|
+
|
|
103
|
+
if (props.ensureActionsColumn && hasAnyAction.value && !hasActionsCol) {
|
|
104
|
+
cols.push({
|
|
105
|
+
name: 'actions',
|
|
106
|
+
label: 'Acciones',
|
|
107
|
+
field: 'actions',
|
|
108
|
+
align: 'right',
|
|
109
|
+
sortable: false
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return cols
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
async function fetchData(pag = pagination.value) {
|
|
117
|
+
loading.value = true
|
|
118
|
+
try {
|
|
119
|
+
// loadAction recibe info de paginación/sort
|
|
120
|
+
const result = await props.loadAction({
|
|
121
|
+
page: pag.page,
|
|
122
|
+
rowsPerPage: pag.rowsPerPage,
|
|
123
|
+
sortBy: pag.sortBy,
|
|
124
|
+
descending: pag.descending
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
if (Array.isArray(result)) {
|
|
128
|
+
rows.value = result
|
|
129
|
+
pagination.value = { ...pag, rowsNumber: result.length }
|
|
130
|
+
emit('loaded', rows.value, { total: result.length })
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const newRows = result?.rows ?? []
|
|
135
|
+
const total = result?.total ?? result?.rowsNumber ?? 0
|
|
136
|
+
|
|
137
|
+
rows.value = newRows
|
|
138
|
+
pagination.value = { ...pag, rowsNumber: total }
|
|
139
|
+
|
|
140
|
+
emit('loaded', rows.value, { total })
|
|
141
|
+
} catch (err) {
|
|
142
|
+
emit('error', err)
|
|
143
|
+
} finally {
|
|
144
|
+
loading.value = false
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function onRequest(req) {
|
|
149
|
+
const nextPag = { ...pagination.value, ...(req?.pagination || {}) }
|
|
150
|
+
pagination.value = nextPag
|
|
151
|
+
await fetchData(nextPag)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function refresh() {
|
|
155
|
+
return fetchData(pagination.value)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function getSelection() {
|
|
159
|
+
return selected.value
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
defineExpose({ refresh, getSelection })
|
|
163
|
+
|
|
164
|
+
onMounted(() => {
|
|
165
|
+
fetchData(pagination.value)
|
|
166
|
+
})
|
|
167
|
+
</script>
|