1
+ <script setup>
2
+ import { computed , ref , watch } from ' vue' ;
3
+
4
+ const props = defineProps ({
5
+ data: {
6
+ type: Array ,
7
+ required: true
8
+ },
9
+ columns: {
10
+ type: Array ,
11
+ required: true
12
+ },
13
+ itemsPerPage: {
14
+ type: Number ,
15
+ default: 10
16
+ }
17
+ });
18
+
19
+ const currentPage = ref (1 );
20
+
21
+ const totalItems = computed (() => props .data .length );
22
+ const totalPages = computed (() => Math .ceil (totalItems .value / props .itemsPerPage ));
23
+
24
+ const startIndex = computed (() => (currentPage .value - 1 ) * props .itemsPerPage );
25
+ const endIndex = computed (() => Math .min (startIndex .value + props .itemsPerPage , totalItems .value ));
26
+
27
+ const paginatedData = computed (() => {
28
+ return props .data .slice (startIndex .value , endIndex .value );
29
+ });
30
+
31
+ const displayedPages = computed (() => {
32
+ const delta = 2 ;
33
+ const range = [];
34
+ const rangeWithDots = [];
35
+ let l;
36
+
37
+ for (let i = 1 ; i <= totalPages .value ; i++ ) {
38
+ if (i === 1 || i === totalPages .value ||
39
+ (i >= currentPage .value - delta && i <= currentPage .value + delta)) {
40
+ range .push (i);
41
+ }
42
+ }
43
+
44
+ range .forEach (i => {
45
+ if (l) {
46
+ if (i - l === 2 ) {
47
+ rangeWithDots .push (l + 1 );
48
+ } else if (i - l !== 1 ) {
49
+ rangeWithDots .push (' ...' );
50
+ }
51
+ }
52
+ rangeWithDots .push (i);
53
+ l = i;
54
+ });
55
+
56
+ return rangeWithDots;
57
+ });
58
+
59
+ const previousPage = () => {
60
+ if (currentPage .value > 1 ) {
61
+ currentPage .value -- ;
62
+ }
63
+ };
64
+
65
+ const nextPage = () => {
66
+ if (currentPage .value < totalPages .value ) {
67
+ currentPage .value ++ ;
68
+ }
69
+ };
70
+
71
+ const goToPage = (page ) => {
72
+ if (typeof page === ' number' ) {
73
+ currentPage .value = page;
74
+ }
75
+ };
76
+
77
+ watch (() => props .data , () => {
78
+ currentPage .value = 1 ;
79
+ });
80
+ </script >
81
+
82
+ <template >
83
+ <div class =" overflow-x-auto bg-white rounded-lg shadow" >
84
+ <table class =" min-w-full border-collapse rounded-lg overflow-hidden" >
85
+ <thead class =" bg-red-500" >
86
+ <tr >
87
+ <th v-for =" (column, index) in columns"
88
+ :key =" column.key"
89
+ class =" px-6 py-3 text-left text-xs text-white font-medium text-gray-700 uppercase tracking-wider border-b border-gray-200" >
90
+ {{ column.label }}
91
+ </th >
92
+ </tr >
93
+ </thead >
94
+ <tbody class =" bg-white" >
95
+ <tr v-for =" item in paginatedData"
96
+ :key =" item.id"
97
+ class =" hover:bg-gray-50 border-b border-gray-200" >
98
+ <td v-for =" column in columns"
99
+ :key =" column.key"
100
+ class =" px-6 py-4 whitespace-nowrap text-sm text-gray-900" >
101
+ {{ item[column.key] }}
102
+ </td >
103
+ </tr >
104
+ </tbody >
105
+ </table >
106
+
107
+ <!-- Paginación -->
108
+ <div class =" bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6" >
109
+ <div class =" flex-1 flex justify-between sm:hidden" >
110
+ <button @click =" previousPage"
111
+ :disabled =" currentPage === 1"
112
+ class =" relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
113
+ :class =" { 'opacity-50 cursor-not-allowed': currentPage === 1 }" >
114
+ Anterior
115
+ </button >
116
+ <button @click =" nextPage"
117
+ :disabled =" currentPage >= totalPages"
118
+ class =" ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
119
+ :class =" { 'opacity-50 cursor-not-allowed': currentPage >= totalPages }" >
120
+ Siguiente
121
+ </button >
122
+ </div >
123
+ <div class =" hidden sm:flex-1 sm:flex sm:items-center sm:justify-between" >
124
+ <div >
125
+ <p class =" text-sm text-gray-700" >
126
+ Mostrando
127
+ <span class =" font-medium" >{{ startIndex + 1 }}</span >
128
+ a
129
+ <span class =" font-medium" >{{ endIndex }}</span >
130
+ de
131
+ <span class =" font-medium" >{{ totalItems }}</span >
132
+ resultados
133
+ </p >
134
+ </div >
135
+ <div >
136
+ <nav class =" relative z-0 inline-flex rounded-md shadow-sm -space-x-px" >
137
+ <button @click =" previousPage"
138
+ :disabled =" currentPage === 1"
139
+ class =" relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50"
140
+ :class =" { 'opacity-50 cursor-not-allowed': currentPage === 1 }" >
141
+ <span class =" sr-only" >Anterior</span >
142
+ <svg class =" h-5 w-5" xmlns =" http://www.w3.org/2000/svg" viewBox =" 0 0 20 20" fill =" currentColor" >
143
+ <path fill-rule =" evenodd" d =" M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule =" evenodd" />
144
+ </svg >
145
+ </button >
146
+ <button v-for =" page in displayedPages"
147
+ :key =" page"
148
+ @click =" goToPage(page)"
149
+ :class =" [
150
+ currentPage === page
151
+ ? 'z-10 bg-gray-50 border-gray-300 text-gray-600'
152
+ : 'bg-white border-gray-300 text-gray-500 hover:bg-gray-50',
153
+ 'relative inline-flex items-center px-4 py-2 border text-sm font-medium'
154
+ ]" >
155
+ {{ page }}
156
+ </button >
157
+ <button @click =" nextPage"
158
+ :disabled =" currentPage >= totalPages"
159
+ class =" relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50"
160
+ :class =" { 'opacity-50 cursor-not-allowed': currentPage >= totalPages }" >
161
+ <span class =" sr-only" >Siguiente</span >
162
+ <svg class =" h-5 w-5" xmlns =" http://www.w3.org/2000/svg" viewBox =" 0 0 20 20" fill =" currentColor" >
163
+ <path fill-rule =" evenodd" d =" M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule =" evenodd" />
164
+ </svg >
165
+ </button >
166
+ </nav >
167
+ </div >
168
+ </div >
169
+ </div >
170
+ </div >
171
+ </template >
0 commit comments