286286 </div >
287287 </div >
288288
289- <!-- Footer -->
290- <div class =" px-4 py-3 bg-[#0d0d16] border-t border-white/5 flex items-center justify-between" >
291- <span class =" text-xs text-gray-600" >
292- Showing <span class =" text-gray-400" >{{ store.issues.length }}</span > of <span class =" text-gray-400" >{{ store.total }}</span >
289+ <!-- Pagination footer -->
290+ <div class =" px-4 py-3 bg-[#0d0d16] border-t border-white/5 flex items-center justify-between gap-4" >
291+
292+ <!-- Row count -->
293+ <span class =" text-xs text-gray-600 shrink-0" >
294+ <span class =" text-gray-400" >{{ pageStart }}–{{ pageEnd }}</span >
295+ of
296+ <span class =" text-gray-400" >{{ store.total }}</span >
293297 </span >
294- <button
295- v-if =" store.hasMore"
296- @click =" loadMore"
297- :disabled =" store.loadingMore"
298- class =" text-xs text-violet-400 hover:text-violet-300 disabled:opacity-50 transition-colors font-medium"
299- >
300- {{ store.loadingMore ? 'Loading…' : 'Load more' }}
301- </button >
298+
299+ <!-- Page buttons -->
300+ <div class =" flex items-center gap-1" >
301+ <!-- Prev -->
302+ <button
303+ @click =" goToPage(store.page - 1)"
304+ :disabled =" store.page <= 1"
305+ class =" w-7 h-7 flex items-center justify-center rounded-md text-gray-400 hover:text-white hover:bg-white/8
306+ disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
307+ aria-label =" Previous page"
308+ >
309+ <svg width =" 14" height =" 14" viewBox =" 0 0 16 16" fill =" none" stroke =" currentColor" stroke-width =" 2" stroke-linecap =" round" stroke-linejoin =" round" >
310+ <polyline points =" 10 4 6 8 10 12" />
311+ </svg >
312+ </button >
313+
314+ <!-- Page numbers -->
315+ <span
316+ v-for =" (p, i) in pageButtons"
317+ :key =" i"
318+ >
319+ <span v-if =" p === '...'" class =" w-7 h-7 flex items-center justify-center text-xs text-gray-600" >…</span >
320+ <button
321+ v-else
322+ @click =" goToPage(p)"
323+ :class =" p === store.page
324+ ? 'bg-violet-600 text-white shadow'
325+ : 'text-gray-400 hover:text-white hover:bg-white/8'"
326+ class =" w-7 h-7 flex items-center justify-center rounded-md text-xs font-medium transition-colors tabular-nums"
327+ >
328+ {{ p }}
329+ </button >
330+ </span >
331+
332+ <!-- Next -->
333+ <button
334+ @click =" goToPage(store.page + 1)"
335+ :disabled =" store.page >= store.totalPages"
336+ class =" w-7 h-7 flex items-center justify-center rounded-md text-gray-400 hover:text-white hover:bg-white/8
337+ disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
338+ aria-label =" Next page"
339+ >
340+ <svg width =" 14" height =" 14" viewBox =" 0 0 16 16" fill =" none" stroke =" currentColor" stroke-width =" 2" stroke-linecap =" round" stroke-linejoin =" round" >
341+ <polyline points =" 6 4 10 8 6 12" />
342+ </svg >
343+ </button >
344+ </div >
345+
346+ <!-- Per-page label -->
347+ <span class =" text-xs text-gray-600 shrink-0 hidden sm:block" >
348+ {{ store.perPage }} / page
349+ </span >
350+
302351 </div >
303352 </div >
304353
@@ -330,6 +379,7 @@ const tab = ref(route.query.tab ?? 'unresolved')
330379const search = ref (route .query .search ?? ' ' )
331380const environment = ref (route .query .env ?? ' ' )
332381const release = ref (route .query .release ?? ' ' )
382+ const page = ref (Number (route .query .page ?? 1 ))
333383const releases = ref ([])
334384const openMenu = ref (null )
335385const menuConfirm = ref (null ) // { id, type } — pending confirmation for ellipsis action
@@ -343,10 +393,11 @@ function activeSearch() { return tab.value === 'vitals' ? 'Performance vitals'
343393
344394function syncQuery () {
345395 router .replace ({ query: {
346- ... (tab .value !== ' unresolved' ? { tab: tab .value } : {}),
347- ... (search .value ? { search: search .value } : {}),
396+ ... (tab .value !== ' unresolved' ? { tab: tab .value } : {}),
397+ ... (search .value ? { search: search .value } : {}),
348398 ... (environment .value ? { env: environment .value } : {}),
349399 ... (release .value ? { release: release .value } : {}),
400+ ... (page .value > 1 ? { page: page .value } : {}),
350401 }})
351402}
352403
@@ -355,6 +406,7 @@ function fetchIssues() {
355406 search: activeSearch (),
356407 environment: environment .value ,
357408 release: release .value ,
409+ page: page .value ,
358410 })
359411}
360412
@@ -407,12 +459,14 @@ async function action(type, id) {
407459// ── Tab / filter changes ──────────────────────────────────────────────────────
408460function setTab (t ) {
409461 tab .value = t
462+ page .value = 1
410463 selected .value = new Set ()
411464 syncQuery ()
412465 fetchIssues ()
413466}
414467
415468function refetch () {
469+ page .value = 1
416470 selected .value = new Set ()
417471 syncQuery ()
418472 fetchIssues ()
@@ -423,10 +477,37 @@ function onSearch() {
423477 searchTimer = setTimeout (refetch, 300 )
424478}
425479
426- function loadMore () {
427- store .fetchMore (route .params .id , activeStatus (), activeSearch (), environment .value , release .value )
480+ function goToPage (p ) {
481+ if (p < 1 || p > store .totalPages || p === store .page ) return
482+ page .value = p
483+ selected .value = new Set ()
484+ syncQuery ()
485+ fetchIssues ()
486+ window .scrollTo ({ top: 0 , behavior: ' smooth' })
428487}
429488
489+ // ── Pagination helpers ────────────────────────────────────────────────────────
490+ const pageStart = computed (() => store .total === 0 ? 0 : (store .page - 1 ) * store .perPage + 1 )
491+ const pageEnd = computed (() => Math .min (store .page * store .perPage , store .total ))
492+
493+ // Builds the page button list with ellipsis, e.g. [1, '...', 4, 5, 6, '...', 12]
494+ const pageButtons = computed (() => {
495+ const total = store .totalPages
496+ const cur = store .page
497+ if (total <= 7 ) return Array .from ({ length: total }, (_ , i ) => i + 1 )
498+
499+ const pages = new Set ([1 , total, cur - 1 , cur, cur + 1 ].filter (p => p >= 1 && p <= total))
500+ const sorted = [... pages].sort ((a , b ) => a - b)
501+ const result = []
502+ let prev = 0
503+ for (const p of sorted) {
504+ if (p - prev > 1 ) result .push (' ...' )
505+ result .push (p)
506+ prev = p
507+ }
508+ return result
509+ })
510+
430511// ── Bulk selection ────────────────────────────────────────────────────────────
431512const allSelected = computed (() => store .issues .length > 0 && store .issues .every (i => selected .value .has (i .id )))
432513const someSelected = computed (() => ! allSelected .value && store .issues .some (i => selected .value .has (i .id )))
0 commit comments