feat: 新增分页组件
This commit is contained in:
167
src/renderer/components/Pagination/index.vue
Normal file
167
src/renderer/components/Pagination/index.vue
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex items-center justify-between py-4">
|
||||||
|
<div class="text-sm text-gray-600">共 {{ total }} 项数据</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<SelectRoot :modelValue="String(pageSize)" @update:modelValue="onPageSizeChange">
|
||||||
|
<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>
|
||||||
|
<SelectIcon>
|
||||||
|
<RiArrowDownSLine />
|
||||||
|
</SelectIcon>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectPortal>
|
||||||
|
<SelectContent class="z-50 min-w-[120px] rounded-md border border-gray-200 bg-white shadow-lg">
|
||||||
|
<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">
|
||||||
|
<SelectItemText>{{ s }} 条/页</SelectItemText>
|
||||||
|
</SelectItem>
|
||||||
|
</SelectViewport>
|
||||||
|
</SelectContent>
|
||||||
|
</SelectPortal>
|
||||||
|
</SelectRoot>
|
||||||
|
|
||||||
|
<button
|
||||||
|
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"
|
||||||
|
:disabled="page === 1"
|
||||||
|
@click="go(page - 1)"
|
||||||
|
aria-label="上一页"
|
||||||
|
>
|
||||||
|
<RiArrowLeftLine />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<template v-for="p in pages" :key="String(p) + '-' + page">
|
||||||
|
<button
|
||||||
|
v-if="typeof p === 'number'"
|
||||||
|
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"
|
||||||
|
:class="p === page ? 'border-blue-500 text-blue-600' : 'text-gray-700'"
|
||||||
|
@click="go(p)"
|
||||||
|
>
|
||||||
|
{{ p }}
|
||||||
|
</button>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
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"
|
||||||
|
:disabled="page === totalPages"
|
||||||
|
@click="go(page + 1)"
|
||||||
|
aria-label="下一页"
|
||||||
|
>
|
||||||
|
<RiArrowRightLine />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="ml-2 flex items-center gap-2 text-sm text-gray-700">
|
||||||
|
<span>跳至</span>
|
||||||
|
<input
|
||||||
|
class="w-16 rounded border border-gray-200 bg-white px-2 py-1 text-center outline-none focus:border-blue-500"
|
||||||
|
type="text"
|
||||||
|
:min="1"
|
||||||
|
:max="totalPages"
|
||||||
|
v-model.number="jumpValue"
|
||||||
|
@keydown.enter="onJump"
|
||||||
|
@blur="onJump"
|
||||||
|
/>
|
||||||
|
<span>/ {{ totalPages }} 页</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts" name="Pagination">
|
||||||
|
import { computed, watch, ref, defineProps, defineEmits } from 'vue'
|
||||||
|
import { RiArrowLeftLine, RiArrowRightLine, RiArrowDownSLine } from '@remixicon/vue'
|
||||||
|
import { SelectRoot, SelectTrigger, SelectIcon, SelectPortal, SelectContent, SelectViewport, SelectItem, SelectItemText } from 'radix-vue'
|
||||||
|
import { PaginationProps, EmitsProps } from '@/types'
|
||||||
|
|
||||||
|
const props = defineProps<PaginationProps>()
|
||||||
|
|
||||||
|
const emit = defineEmits<EmitsProps>()
|
||||||
|
|
||||||
|
const pageSizes = computed(() => (props.pageSizes && props.pageSizes.length > 0 ? props.pageSizes : [10, 20, 50, 100]))
|
||||||
|
const siblingCount = computed(() => (typeof props.siblingCount === 'number' ? props.siblingCount : 1))
|
||||||
|
const boundaryCount = computed(() => (typeof props.boundaryCount === 'number' ? props.boundaryCount : 1))
|
||||||
|
|
||||||
|
const totalPages = computed(() => {
|
||||||
|
const size = Math.max(1, props.pageSize)
|
||||||
|
const total = Math.max(0, props.total)
|
||||||
|
const pages = Math.ceil(total / size)
|
||||||
|
return pages > 0 ? pages : 1
|
||||||
|
})
|
||||||
|
|
||||||
|
const page = computed(() => {
|
||||||
|
const p = Math.max(1, props.page)
|
||||||
|
return Math.min(p, totalPages.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const range = (start: number, end: number) => Array.from({ length: end - start + 1 }, (_, i) => start + i)
|
||||||
|
|
||||||
|
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 = () => {
|
||||||
|
go(jumpValue.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onPageSizeChange = (val: string) => {
|
||||||
|
const size = Number(val)
|
||||||
|
if (size && size !== props.pageSize) {
|
||||||
|
emit('update:pageSize', size)
|
||||||
|
emit('pageSizeChange', size)
|
||||||
|
emit('update:page', 1)
|
||||||
|
emit('pageChange', 1)
|
||||||
|
jumpValue.value = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.page,
|
||||||
|
(p) => {
|
||||||
|
const target = Math.max(1, Math.min(p, totalPages.value))
|
||||||
|
jumpValue.value = target
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style></style>
|
||||||
15
src/renderer/types/PaginationProps.ts
Normal file
15
src/renderer/types/PaginationProps.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
export interface PaginationProps {
|
||||||
|
total: number
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
pageSizes?: number[]
|
||||||
|
siblingCount?: number
|
||||||
|
boundaryCount?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmitsProps {
|
||||||
|
(e: 'update:page', value: number): void
|
||||||
|
(e: 'update:pageSize', value: number): void
|
||||||
|
(e: 'pageChange', value: number): void
|
||||||
|
(e: 'pageSizeChange', value: number): void
|
||||||
|
}
|
||||||
3
src/renderer/types/index.ts
Normal file
3
src/renderer/types/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { PaginationProps, EmitsProps } from './PaginationProps'
|
||||||
|
|
||||||
|
export { PaginationProps, EmitsProps }
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<template>
|
||||||
|
<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" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts" name="RateContentSection">
|
||||||
|
import Pagination from '@/components/Pagination/index.vue'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style></style>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="grid grid-cols-5 gap-[15px]">
|
<div class="grid grid-cols-5 gap-[15px] mb-[20px]">
|
||||||
<div class="bg-[#F5F7FA] box-border flex flex-col p-[16px] rounded-[12px]" v-for="item in channels" :key="item.id">
|
<div class="bg-[#F5F7FA] box-border flex flex-col p-[16px] rounded-[12px]" v-for="item in channels" :key="item.id">
|
||||||
<div class="flex items-center mb-[8px]">
|
<div class="flex items-center mb-[8px]">
|
||||||
<img :src="item.icon" class="w-[24px] h-[24px] mr-[8px]">
|
<img :src="item.icon" class="w-[24px] h-[24px] mr-[8px]">
|
||||||
@@ -20,8 +20,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts" name="RatePanelSection">
|
||||||
import { channels } from '@constant/rate'
|
import { channels } from '@constant/rate'
|
||||||
import { RiStarFill } from '@remixicon/vue'
|
import { RiStarFill } from '@remixicon/vue'
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
BIN
src/renderer/views/rate/images/2.jpg
Normal file
BIN
src/renderer/views/rate/images/2.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
@@ -3,6 +3,7 @@
|
|||||||
<div class="bg-white box-border w-[936px] h-full rounded-[16px] p-[20px]">
|
<div class="bg-white box-border w-[936px] h-full rounded-[16px] p-[20px]">
|
||||||
<RateTitleSection />
|
<RateTitleSection />
|
||||||
<RatePanelSection />
|
<RatePanelSection />
|
||||||
|
<RateContentSection />
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
</template>
|
</template>
|
||||||
@@ -10,5 +11,6 @@
|
|||||||
<script setup lang="ts" name="Rate">
|
<script setup lang="ts" name="Rate">
|
||||||
import RateTitleSection from './components/RateTitleSection/index.vue'
|
import RateTitleSection from './components/RateTitleSection/index.vue'
|
||||||
import RatePanelSection from './components/RatePanelSection/index.vue'
|
import RatePanelSection from './components/RatePanelSection/index.vue'
|
||||||
|
import RateContentSection from './components/RateContentSection/index.vue'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -15,11 +15,13 @@
|
|||||||
"strict": true,
|
"strict": true,
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"],
|
"@/*": ["src/renderer/*"],
|
||||||
|
"@assets/*": ["src/assets/*"],
|
||||||
"@store/*": ["src/renderer/store/*"],
|
"@store/*": ["src/renderer/store/*"],
|
||||||
"@constant/*": ["src/renderer/constant/*"],
|
"@constant/*": ["src/renderer/constant/*"],
|
||||||
"@utils/*": ["src/renderer/utils/*"],
|
"@utils/*": ["src/renderer/utils/*"],
|
||||||
"@api/*": ["src/renderer/api/*"],
|
"@api/*": ["src/renderer/api/*"],
|
||||||
|
"@/types": ["src/renderer/types/index.ts"],
|
||||||
"@modules/*": ["src/electron/main/modules/*"],
|
"@modules/*": ["src/electron/main/modules/*"],
|
||||||
},
|
},
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export default defineConfig({
|
|||||||
"@constant": resolve(__dirname, "./src/renderer/constant"),
|
"@constant": resolve(__dirname, "./src/renderer/constant"),
|
||||||
"@utils": resolve(__dirname, "./src/renderer/utils"),
|
"@utils": resolve(__dirname, "./src/renderer/utils"),
|
||||||
"@api": resolve(__dirname, "./src/renderer/api"),
|
"@api": resolve(__dirname, "./src/renderer/api"),
|
||||||
|
"@/types": resolve(__dirname, "./src/renderer/types"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user