feat: 分页插件组件封装

This commit is contained in:
duanshuwen
2025-12-06 10:38:35 +08:00
parent d0162e3349
commit 5dc7d73120
3 changed files with 112 additions and 130 deletions

View File

@@ -1,69 +1,77 @@
<template> <template>
<div class="flex items-center justify-between py-4"> <div class="flex items-center justify-between pt-[20px]">
<div class="text-sm text-gray-600"> {{ total }} 项数据</div> <div class="text-sm text-gray-600"> {{ total }} 项数据</div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<SelectRoot :modelValue="String(pageSize)" @update:modelValue="onPageSizeChange"> <SelectRoot v-model="selectedCount" v-model:open="open">
<SelectTrigger class="inline-flex items-center justify-between rounded-md border border-gray-200 bg-white px-3 py-1 text-sm text-gray-700 shadow-sm hover:bg-gray-50"> <SelectTrigger class="inline-flex items-center justify-between rounded-md border border-gray-200 bg-white px-3 py-1 text-sm text-gray-700 shadow-sm hover:bg-gray-50">
<span>{{ pageSize }} /</span> <span>{{ selectedCount }} /</span>
<SelectIcon> <RiArrowDownSLine />
<RiArrowDownSLine />
</SelectIcon>
</SelectTrigger> </SelectTrigger>
<SelectPortal>
<SelectContent class="z-50 min-w-[120px] rounded-md border border-gray-200 bg-white shadow-lg"> <SelectPortal :to="portalTarget">
<SelectContent position="popper" :side-offset="6" class="z-50 rounded-md border border-gray-200 bg-white shadow-lg" :style="{ minWidth: 'var(--radix-select-trigger-width)' }">
<SelectViewport class="p-1"> <SelectViewport class="p-1">
<SelectItem v-for="s in pageSizes" :key="s" :value="String(s)" class="flex cursor-pointer select-none items-center rounded px-2 py-1 text-sm text-gray-700 hover:bg-gray-50"> <SelectGroup>
<SelectItemText>{{ s }} /</SelectItemText> <SelectItem v-for="(option, index) in pageSizes" :key="index" :value="String(option)" class="flex cursor-pointer select-none items-center rounded px-2 py-1 text-sm text-gray-700 hover:bg-gray-50">
</SelectItem> <SelectItemText>{{ option }} /</SelectItemText>
</SelectItem>
</SelectGroup>
</SelectViewport> </SelectViewport>
</SelectContent> </SelectContent>
</SelectPortal> </SelectPortal>
</SelectRoot> </SelectRoot>
<button <PaginationRoot
class="inline-flex h-8 w-8 items-center justify-center rounded border border-gray-200 bg-white text-gray-700 shadow-sm hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-50" v-model:page="currentPage"
:disabled="page === 1" :total="total"
@click="go(page - 1)" :items-per-page="Number(selectedCount)"
aria-label="上一页" show-edges
:sibling-count="1"
:default-page="currentPage"
> >
<RiArrowLeftLine /> <PaginationList
</button> v-slot="{ items }"
class="flex items-center gap-1 text-[#171717]"
<div class="flex items-center gap-1"> >
<template v-for="p in pages" :key="String(p) + '-' + page"> <PaginationPrev aria-label="上一页" class="w-9 h-9 flex items-center justify-center mr-4 border rounded bg-white text-gray-700 disabled:opacity-50">
<button <RiArrowLeftLine size="18" />
v-if="typeof p === 'number'" </PaginationPrev>
class="inline-flex h-8 min-w-[32px] items-center justify-center rounded border border-gray-200 bg-white px-2 text-sm shadow-sm hover:bg-gray-50" <template v-for="(page, index) in items">
:class="p === page ? 'border-blue-500 text-blue-600' : 'text-gray-700'" <PaginationListItem
@click="go(p)" v-if="page.type === 'page'"
> :key="index"
{{ p }} class="w-9 h-9 border rounded transition data-[selected]:bg-blue-600 data-[selected]:text-white hover:bg-white/10"
</button> :value="page.value"
<span v-else class="inline-flex h-8 min-w-[32px] items-center justify-center rounded border border-transparent px-2 text-sm text-gray-400"></span> >
</template> {{ page.value }}
</div> </PaginationListItem>
<PaginationEllipsis
<button v-else
class="inline-flex h-8 w-8 items-center justify-center rounded border border-gray-200 bg-white text-gray-700 shadow-sm hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-50" :key="page.type"
:disabled="page === totalPages" :index="index"
@click="go(page + 1)" class="w-9 h-9 flex items-center justify-center"
aria-label="下一页" >
> &#8230;
<RiArrowRightLine /> </PaginationEllipsis>
</button> </template>
<PaginationNext aria-label="下一页" class="w-9 h-9 flex items-center justify-center ml-4 border rounded bg-white text-gray-700 disabled:opacity-50 focus-within:outline focus-within:outline-1 focus-within:outline-offset-1">
<RiArrowRightLine size="18" />
</PaginationNext>
</PaginationList>
</PaginationRoot>
<div class="ml-2 flex items-center gap-2 text-sm text-gray-700"> <div class="ml-2 flex items-center gap-2 text-sm text-gray-700">
<span>跳至</span> <span>跳至</span>
<input <input
class="w-16 rounded border border-gray-200 bg-white px-2 py-1 text-center outline-none focus:border-blue-500" class="w-[55px] rounded border border-gray-200 bg-white px-2 py-1 text-center outline-none focus:border-blue-500"
type="text" type="text"
:min="1" :min="1"
:max="totalPages" :max="pageTotal"
v-model.number="jumpValue" v-model.number="jumpValue"
@keydown.enter="onJump" @keydown.enter="onJump"
@blur="onJump" @blur="onJump"
/> />
<span>/ {{ totalPages }} </span> <span>/ {{ pageTotal }} </span>
</div> </div>
</div> </div>
</div> </div>
@@ -71,97 +79,57 @@
</template> </template>
<script setup lang="ts" name="Pagination"> <script setup lang="ts" name="Pagination">
import { computed, watch, ref, defineProps, defineEmits } from 'vue' import { ref, defineProps, defineEmits, watch, onMounted, computed } from 'vue'
import { RiArrowLeftLine, RiArrowRightLine, RiArrowDownSLine } from '@remixicon/vue' import { RiArrowLeftLine, RiArrowRightLine, RiArrowDownSLine } from '@remixicon/vue'
import { SelectRoot, SelectTrigger, SelectIcon, SelectPortal, SelectContent, SelectViewport, SelectItem, SelectItemText } from 'radix-vue' import { SelectRoot, SelectTrigger, SelectPortal, SelectContent, SelectViewport, SelectGroup, SelectItem, SelectItemText, PaginationRoot, PaginationList, PaginationPrev, PaginationListItem, PaginationEllipsis, PaginationNext } from 'radix-vue'
import { PaginationProps, EmitsProps } from '@/types' import { PaginationProps, EmitsProps } from '@/types'
const props = defineProps<PaginationProps>() const props = defineProps<PaginationProps>()
const emit = defineEmits<EmitsProps>() const emit = defineEmits<EmitsProps>()
const pageSizes = computed(() => (props.pageSizes && props.pageSizes.length > 0 ? props.pageSizes : [10, 20, 50, 100])) const pageSizes = ref(Array.isArray(props.pageSizes) && props.pageSizes.length ? props.pageSizes : [10, 20, 50, 100, 200])
const siblingCount = computed(() => (typeof props.siblingCount === 'number' ? props.siblingCount : 1)) const selectedCount = ref(String(props.pageSize ?? pageSizes.value[0]))
const boundaryCount = computed(() => (typeof props.boundaryCount === 'number' ? props.boundaryCount : 1)) const open = ref(false)
const portalTarget = typeof document !== 'undefined' ? document.body : undefined
const totalPages = computed(() => { watch(() => props.pageSize, (val) => selectedCount.value = String(val))
const size = Math.max(1, props.pageSize) watch(selectedCount, (val) => emit('update:pageSize', val))
const total = Math.max(0, props.total) watch(open, (val) => {
const pages = Math.ceil(total / size) console.log('Select open state:', val)
return pages > 0 ? pages : 1
}) })
const page = computed(() => { onMounted(() => {
const p = Math.max(1, props.page) console.log('Select initial value:', selectedCount.value)
return Math.min(p, totalPages.value) })
const pageTotal = computed(() => {
const t = Number(props.total || 0)
const s = Number(selectedCount.value || props.pageSize || 10)
const pages = s > 0 ? Math.ceil(t / s) : 1
return Math.max(1, pages)
})
watch(pageTotal, (val) => {
console.log('Pagination total pages:', val, 'from total items:', props.total, 'pageSize:', selectedCount.value)
}) })
const range = (start: number, end: number) => Array.from({ length: end - start + 1 }, (_, i) => start + i) // 跳转分页
const jumpValue = ref()
const pages = computed<(number | string)[]>(() => {
const total = totalPages.value
const current = page.value
const siblings = siblingCount.value
const boundaries = boundaryCount.value
if (total <= 1) return [1]
const startPages = range(1, Math.min(boundaries, total))
const endPages = range(Math.max(total - boundaries + 1, boundaries + 1), total)
const siblingsStart = Math.max(
Math.min(current - siblings, total - boundaries - siblings * 2 - 1),
boundaries + 2
)
const siblingsEnd = Math.min(
Math.max(current + siblings, boundaries + siblings * 2 + 2),
endPages.length > 0 ? endPages[0] - 2 : total - 1
)
const middlePages = siblingsStart <= siblingsEnd ? range(siblingsStart, siblingsEnd) : []
const items: (number | string)[] = [...startPages]
if (middlePages.length && startPages[startPages.length - 1] + 1 < middlePages[0]) items.push('…')
items.push(...middlePages)
if (endPages.length && middlePages[middlePages.length - 1] + 1 < endPages[0]) items.push('…')
items.push(...endPages)
return items
})
const jumpValue = ref<number>(page.value)
const go = (p: number) => {
const target = Math.max(1, Math.min(p, totalPages.value))
if (target !== props.page) {
emit('update:page', target)
emit('pageChange', target)
}
jumpValue.value = target
}
const onJump = () => { const onJump = () => {
go(jumpValue.value) const v = Number(jumpValue.value)
if (!Number.isFinite(v)) return
const clamped = Math.max(1, Math.min(pageTotal.value, v))
jumpValue.value = clamped
currentPage.value = clamped
emit('update:currentPage', clamped)
emit('currentChange', clamped)
} }
const onPageSizeChange = (val: string) => { const currentPage = ref(props.currentPage ?? 1)
const size = Number(val) watch(() => props.currentPage, (val) => {
if (size && size !== props.pageSize) { if (typeof val === 'number' && Number.isFinite(val)) currentPage.value = val
emit('update:pageSize', size) })
emit('pageSizeChange', size) watch(currentPage, (p) => {
emit('update:page', 1) emit('update:currentPage', p)
emit('pageChange', 1) emit('currentChange', p)
jumpValue.value = 1 })
}
}
watch(
() => props.page,
(p) => {
const target = Math.max(1, Math.min(p, totalPages.value))
jumpValue.value = target
}
)
</script> </script>
<style></style> <style></style>

View File

@@ -1,15 +1,12 @@
export interface PaginationProps { export interface PaginationProps {
currentPage?: number
total: number total: number
page: number
pageSize: number pageSize: number
pageSizes?: number[] pageSizes?: number[]
siblingCount?: number
boundaryCount?: number
} }
export interface EmitsProps { export interface EmitsProps {
(e: 'update:page', value: number): void (e: 'update:currentPage', value: number): void
(e: 'update:pageSize', value: number): void (e: 'update:pageSize', value: string): void
(e: 'pageChange', value: number): void (e: 'currentChange', value: number): void
(e: 'pageSizeChange', value: number): void
} }

View File

@@ -1,11 +1,28 @@
<template> <template>
<div class="box-border border-[1px] border-[#E5E8EE] rounded-[16px] p-[20px]"> <div class="box-border border-[1px] border-[#E5E8EE] rounded-[16px] p-[20px]">
<Pagination :page="1" :pageSize="10" :total="30" :pageSizes="[10, 20, 30]" :siblingCount="siblingCount" :boundaryCount="boundaryCount" /> <Pagination v-model:current-page="queryParams.pageIndex" :page-size="queryParams.pageSize" :total="total" @update:pageSize="updatePageSize" @update:currentPage="updatePageIndex" />
</div> </div>
</template> </template>
<script setup lang="ts" name="RateContentSection"> <script setup lang="ts" name="RateContentSection">
import { ref } from 'vue'
import Pagination from '@/components/Pagination/index.vue' import Pagination from '@/components/Pagination/index.vue'
const total = ref(200)
const queryParams = ref({
pageIndex: 1,
pageSize: 10,
})
const updatePageSize = (val: string) => {
console.log("🚀 ~ updatePageSize ~ val:", val)
queryParams.value.pageSize = Number(val)
}
const updatePageIndex = (val: number) => {
console.log("🚀 ~ updatePageIndex ~ val:", val)
queryParams.value.pageIndex = val
}
</script> </script>
<style></style> <style></style>