본문 이미지 최적화·라이트박스·번호형 페이지네이션 반영 (v1.10.3)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
145
components/site/SitePagination.vue
Normal file
145
components/site/SitePagination.vue
Normal file
@@ -0,0 +1,145 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
page: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
totalPages: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: '페이지'
|
||||
},
|
||||
basePath: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const normalizedTotalPages = computed(() => Math.max(1, Number(props.totalPages) || 1))
|
||||
const currentPage = computed(() => Math.max(1, Math.min(Number(props.page) || 1, normalizedTotalPages.value)))
|
||||
const previousPage = computed(() => Math.max(1, currentPage.value - 1))
|
||||
const nextPage = computed(() => Math.min(normalizedTotalPages.value, currentPage.value + 1))
|
||||
const canGoPrevious = computed(() => currentPage.value > 1)
|
||||
const canGoNext = computed(() => currentPage.value < normalizedTotalPages.value)
|
||||
|
||||
/**
|
||||
* 연속 페이지 번호를 만든다.
|
||||
* @param {number} start - 시작 페이지
|
||||
* @param {number} end - 끝 페이지
|
||||
* @returns {Array<number>} 페이지 번호 목록
|
||||
*/
|
||||
const range = (start, end) => {
|
||||
const pages = []
|
||||
|
||||
for (let page = start; page <= end; page += 1) {
|
||||
pages.push(page)
|
||||
}
|
||||
|
||||
return pages
|
||||
}
|
||||
|
||||
const pageItems = computed(() => {
|
||||
const total = normalizedTotalPages.value
|
||||
const current = currentPage.value
|
||||
|
||||
if (total <= 8) {
|
||||
return range(1, total).map((page) => ({ type: 'page', page }))
|
||||
}
|
||||
|
||||
const pages = new Set([1, total])
|
||||
|
||||
if (current <= 5) {
|
||||
range(1, Math.min(total - 1, Math.max(4, current + 3))).forEach((page) => pages.add(page))
|
||||
} else if (current >= total - 4) {
|
||||
range(Math.max(2, total - 7), total).forEach((page) => pages.add(page))
|
||||
} else {
|
||||
range(Math.max(2, current - 3), Math.min(total - 1, current + 3)).forEach((page) => pages.add(page))
|
||||
}
|
||||
|
||||
const sortedPages = [...pages].sort((left, right) => left - right)
|
||||
const items = []
|
||||
|
||||
sortedPages.forEach((page, index) => {
|
||||
const previous = sortedPages[index - 1]
|
||||
|
||||
if (previous && page - previous > 1) {
|
||||
items.push({
|
||||
type: 'ellipsis',
|
||||
key: `ellipsis-${previous}-${page}`
|
||||
})
|
||||
}
|
||||
|
||||
items.push({
|
||||
type: 'page',
|
||||
page
|
||||
})
|
||||
})
|
||||
|
||||
return items
|
||||
})
|
||||
|
||||
/**
|
||||
* 페이지 링크 대상을 만든다.
|
||||
* @param {number} page - 페이지 번호
|
||||
* @returns {Object} Nuxt 링크 대상
|
||||
*/
|
||||
const getPageLinkTo = (page) => ({
|
||||
path: props.basePath,
|
||||
query: page > 1 ? { page } : {}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav class="site-pagination site-section" :aria-label="label">
|
||||
<div class="site-pagination__body site-section-body mx-auto flex max-w-[720px] items-center justify-start gap-3 overflow-x-auto text-sm sm:justify-center sm:gap-4">
|
||||
<NuxtLink
|
||||
class="site-pagination__arrow inline-flex size-10 items-center justify-center rounded-full border border-[var(--site-line)] text-[var(--site-muted)] transition hover:border-[var(--site-soft)] hover:text-[var(--site-text)]"
|
||||
:class="!canGoPrevious ? 'pointer-events-none opacity-35' : ''"
|
||||
:to="getPageLinkTo(previousPage)"
|
||||
:aria-disabled="!canGoPrevious"
|
||||
:aria-label="`${label} 이전으로 이동`"
|
||||
>
|
||||
<svg class="size-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="m15 18-6-6 6-6" />
|
||||
</svg>
|
||||
</NuxtLink>
|
||||
|
||||
<ol class="site-pagination__pages flex min-w-max items-center justify-center gap-1 sm:gap-2">
|
||||
<li v-for="item in pageItems" :key="item.type === 'page' ? `page-${item.page}` : item.key">
|
||||
<span
|
||||
v-if="item.type === 'ellipsis'"
|
||||
class="site-pagination__ellipsis inline-flex min-w-9 items-center justify-center px-1 text-base font-semibold text-[var(--site-muted)]"
|
||||
aria-hidden="true"
|
||||
>
|
||||
...
|
||||
</span>
|
||||
<NuxtLink
|
||||
v-else
|
||||
class="site-pagination__page inline-flex min-w-9 items-center justify-center rounded-full px-2 py-2 text-base font-semibold text-[var(--site-muted)] transition hover:text-[var(--site-text)]"
|
||||
:class="item.page === currentPage ? 'site-pagination__page--current pointer-events-none text-[var(--site-text)]' : ''"
|
||||
:to="getPageLinkTo(item.page)"
|
||||
:aria-current="item.page === currentPage ? 'page' : undefined"
|
||||
:aria-label="`${item.page}페이지로 이동`"
|
||||
>
|
||||
{{ item.page }}
|
||||
</NuxtLink>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<NuxtLink
|
||||
class="site-pagination__arrow inline-flex size-10 items-center justify-center rounded-full border border-[var(--site-line)] text-[var(--site-muted)] transition hover:border-[var(--site-soft)] hover:text-[var(--site-text)]"
|
||||
:class="!canGoNext ? 'pointer-events-none opacity-35' : ''"
|
||||
:to="getPageLinkTo(nextPage)"
|
||||
:aria-disabled="!canGoNext"
|
||||
:aria-label="`${label} 다음으로 이동`"
|
||||
>
|
||||
<svg class="size-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="m9 18 6-6-6-6" />
|
||||
</svg>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
Reference in New Issue
Block a user