分页组件

This commit is contained in:
WindowBird 2025-08-25 18:06:33 +08:00
parent 8dfe079690
commit f4e5481108
3 changed files with 548 additions and 71 deletions

View File

@ -0,0 +1,308 @@
<template>
<view class="pagination-container">
<!-- 上拉加载更多模式 -->
<view v-if="mode === 'loadMore'" class="load-more-container">
<view v-if="loading" class="loading-state">
<view class="loading-spinner"></view>
<text class="loading-text">{{ loadingText }}</text>
</view>
<view v-else-if="noMore && list.length > 0" class="no-more-state">
<text class="no-more-text">{{ noMoreText }}</text>
</view>
<view v-else-if="list.length === 0 && !loading" class="empty-state">
<view class="empty-icon">{{ emptyIcon }}</view>
<text class="empty-text">{{ emptyText }}</text>
</view>
</view>
<!-- 分页器模式 -->
<view v-else-if="mode === 'pager'" class="pager-container">
<view v-if="total > 0" class="pager-info">
<text class="pager-text">
{{ total }} {{ currentPage }} / {{ totalPages }}
</text>
</view>
<view class="pager-controls">
<button
class="pager-btn prev-btn"
:disabled="currentPage <= 1"
@click="handlePageChange(currentPage - 1)"
>
上一页
</button>
<view class="page-numbers">
<button
v-for="page in visiblePages"
:key="page"
:class="['page-btn', { active: page === currentPage }]"
@click="handlePageChange(page)"
>
{{ page }}
</button>
</view>
<button
class="pager-btn next-btn"
:disabled="currentPage >= totalPages"
@click="handlePageChange(currentPage + 1)"
>
下一页
</button>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'Pagination',
props: {
// loadMore() pager()
mode: {
type: String,
default: 'loadMore',
validator: value => ['loadMore', 'pager'].includes(value)
},
//
list: {
type: Array,
default: () => []
},
//
total: {
type: Number,
default: 0
},
//
currentPage: {
type: Number,
default: 1
},
//
pageSize: {
type: Number,
default: 10
},
//
loading: {
type: Boolean,
default: false
},
//
noMore: {
type: Boolean,
default: false
},
//
loadingText: {
type: String,
default: '正在加载...'
},
noMoreText: {
type: String,
default: '没有更多数据了'
},
emptyText: {
type: String,
default: '暂无数据'
},
emptyIcon: {
type: String,
default: '📋'
},
//
visiblePageCount: {
type: Number,
default: 5
}
},
computed: {
//
totalPages() {
return Math.ceil(this.total / this.pageSize)
},
//
visiblePages() {
const pages = []
const half = Math.floor(this.visiblePageCount / 2)
let start = Math.max(1, this.currentPage - half)
let end = Math.min(this.totalPages, start + this.visiblePageCount - 1)
//
if (end - start + 1 < this.visiblePageCount) {
start = Math.max(1, end - this.visiblePageCount + 1)
}
for (let i = start; i <= end; i++) {
pages.push(i)
}
return pages
}
},
methods: {
//
handlePageChange(page) {
if (page < 1 || page > this.totalPages || page === this.currentPage) {
return
}
this.$emit('page-change', page)
},
//
reset() {
this.$emit('reset')
}
}
}
</script>
<style lang="scss" scoped>
.pagination-container {
width: 100%;
}
//
.load-more-container {
padding: 20rpx;
text-align: center;
}
.loading-state {
display: flex;
align-items: center;
justify-content: center;
padding: 20rpx 0;
.loading-spinner {
width: 40rpx;
height: 40rpx;
border: 4rpx solid #f3f3f3;
border-top: 4rpx solid #1890ff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-right: 20rpx;
}
.loading-text {
font-size: 28rpx;
color: #666;
}
}
.no-more-state {
padding: 20rpx 0;
.no-more-text {
font-size: 24rpx;
color: #999;
}
}
.empty-state {
padding: 40rpx 0;
.empty-icon {
font-size: 80rpx;
margin-bottom: 20rpx;
}
.empty-text {
font-size: 28rpx;
color: #999;
}
}
//
.pager-container {
padding: 20rpx;
background: #fff;
border-top: 1rpx solid #f0f0f0;
}
.pager-info {
text-align: center;
margin-bottom: 20rpx;
.pager-text {
font-size: 24rpx;
color: #666;
}
}
.pager-controls {
display: flex;
align-items: center;
justify-content: center;
gap: 20rpx;
}
.pager-btn {
min-width: 120rpx;
height: 60rpx;
border: 1rpx solid #d9d9d9;
border-radius: 8rpx;
background: #fff;
color: #333;
font-size: 26rpx;
&:disabled {
color: #ccc;
background: #f5f5f5;
border-color: #d9d9d9;
}
&:not(:disabled):active {
background: #f0f0f0;
}
}
.page-numbers {
display: flex;
gap: 10rpx;
}
.page-btn {
min-width: 60rpx;
height: 60rpx;
border: 1rpx solid #d9d9d9;
border-radius: 8rpx;
background: #fff;
color: #333;
font-size: 26rpx;
&.active {
background: #1890ff;
color: #fff;
border-color: #1890ff;
}
&:not(.active):active {
background: #f0f0f0;
}
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>

View File

@ -0,0 +1,190 @@
import { ref, computed } from 'vue'
/**
* 分页管理组合式函数
* @param {Object} options - 配置选项
* @param {Function} options.fetchData - 获取数据的API函数
* @param {Object} options.defaultParams - 默认查询参数
* @param {string} options.mode - 分页模式'loadMore' 'pager'
* @param {number} options.pageSize - 每页数量
* @returns {Object} 分页相关的状态和方法
*/
export function usePagination(options = {}) {
const {
fetchData,
defaultParams = {},
mode = 'loadMore',
pageSize = 10
} = options
// 基础状态
const list = ref([])
const loading = ref(false)
const error = ref(null)
// 分页参数
const queryParams = ref({
pageNum: 1,
pageSize,
...defaultParams
})
// 分页信息
const pagination = ref({
total: 0,
currentPage: 1,
pageSize,
totalPages: 0
})
// 上拉加载相关
const noMore = ref(false)
// 计算属性
const hasData = computed(() => list.value.length > 0)
const isEmpty = computed(() => !loading.value && list.value.length === 0)
const canLoadMore = computed(() => mode === 'loadMore' && !noMore.value && !loading.value)
/**
* 获取数据列表
* @param {boolean} isRefresh - 是否为刷新操作
*/
const getList = async (isRefresh = false) => {
if (loading.value) return
try {
loading.value = true
error.value = null
// 如果是刷新,重置页码
if (isRefresh) {
queryParams.value.pageNum = 1
noMore.value = false
}
const res = await fetchData(queryParams.value)
// 处理响应数据
const newData = res?.rows || []
const total = res?.total || 0
// 更新分页信息
pagination.value = {
total,
currentPage: queryParams.value.pageNum,
pageSize,
totalPages: Math.ceil(total / pageSize)
}
// 更新数据列表
if (isRefresh || queryParams.value.pageNum === 1) {
list.value = newData
} else {
list.value = [...list.value, ...newData]
}
// 检查是否还有更多数据
if (mode === 'loadMore') {
noMore.value = queryParams.value.pageNum * pageSize >= total
}
console.log(`获取数据成功: 第${queryParams.value.pageNum}页,共${newData.length}`)
} catch (err) {
console.error('获取数据失败:', err)
error.value = err
// 显示错误提示
uni.showToast({
title: '数据加载失败',
icon: 'none'
})
} finally {
loading.value = false
}
}
/**
* 刷新数据
*/
const refresh = () => {
getList(true)
}
/**
* 加载下一页上拉加载模式
*/
const loadMore = () => {
if (!canLoadMore.value) return
queryParams.value.pageNum++
getList()
}
/**
* 跳转到指定页分页器模式
* @param {number} page - 目标页码
*/
const goToPage = (page) => {
if (page < 1 || page > pagination.value.totalPages || page === queryParams.value.pageNum) {
return
}
queryParams.value.pageNum = page
getList(true)
}
/**
* 重置分页状态
*/
const reset = () => {
list.value = []
loading.value = false
error.value = null
noMore.value = false
queryParams.value.pageNum = 1
pagination.value = {
total: 0,
currentPage: 1,
pageSize,
totalPages: 0
}
}
/**
* 更新查询参数
* @param {Object} newParams - 新的查询参数
*/
const updateParams = (newParams) => {
queryParams.value = {
...queryParams.value,
...newParams,
pageNum: 1 // 重置页码
}
reset()
getList()
}
return {
// 状态
list,
loading,
error,
noMore,
pagination,
queryParams,
// 计算属性
hasData,
isEmpty,
canLoadMore,
// 方法
getList,
refresh,
loadMore,
goToPage,
reset,
updateParams
}
}

View File

@ -1,12 +1,12 @@
<template>
<view class="container">
<!-- 加载状态 -->
<view v-if="loading" class="loading-container">
<view v-if="loading && list.length === 0" class="loading-container">
<text class="loading-text">正在加载...</text>
</view>
<!-- 空数据状态 -->
<view v-else-if="list.length === 0 && !loading" class="empty-container">
<view v-else-if="isEmpty" class="empty-container">
<view class="empty-icon">📋</view>
<text class="empty-text">暂无订单数据</text>
</view>
@ -52,23 +52,60 @@
</view>
</view>
</view>
<!-- 加载更多状态 -->
<view v-if="noData && list.length > 0" class="load-more">
<text class="no-more-text">没有更多数据了</text>
</view>
</view>
<!-- 分页组件 -->
<pagination
:mode="'loadMore'"
:list="list"
:loading="loading"
:no-more="noMore"
:total="pagination.total"
:current-page="pagination.currentPage"
:page-size="pagination.pageSize"
@page-change="handlePageChange"
/>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { onMounted, onReachBottom } from '@dcloudio/uni-app'
import { usePagination } from '@/composables/usePagination.js'
import { getMyOrder } from '@/api/order/myOrder.js'
import { onUnload, onReachBottom } from '@dcloudio/uni-app'
import Pagination from '@/components/pagination/pagination.vue'
const list = ref([])
const noData = ref(false)
const loading = ref(false)
// 使
const {
list,
loading,
noMore,
pagination,
getList,
loadMore
} = usePagination({
fetchData: getMyOrder,
defaultParams: {
orderByColumn: 'createTime',
isAsc: 'descending'
},
mode: 'loadMore',
pageSize: 6
})
//
onMounted(() => {
getList()
})
//
onReachBottom(() => {
loadMore()
})
//
const handlePageChange = (page) => {
console.log('页码变化:', page)
}
/**
* 订单状态/物流状态映射工具
@ -96,7 +133,7 @@ function mapOrderStatus(type, value) {
},
}
return maps[type]?.[value] || '未知状态' //
return maps[type]?.[value] || '未知状态'
}
/**
@ -163,57 +200,6 @@ function formatTime(timeStr) {
}
}
//data
const queryParams = {
pageNum: 1,
pageSize: 6,
}
//
onMounted(() => {
getList()
})
onReachBottom(() => {
if (noData.value || loading.value) return
queryParams.pageNum++
getList()
})
//
const getList = async () => {
if (loading.value) return
try {
loading.value = true
let res = await getMyOrder(queryParams)
// res.data
const newData = res?.rows || []
//
if (queryParams.pageNum === 1) {
list.value = newData
} else {
list.value = [...list.value, ...newData]
}
if (queryParams.pageNum * queryParams.pageSize >= res.total) {
noData.value = true
}
console.log('订单列表:', list.value)
} catch (error) {
console.error('获取订单列表失败:', error)
uni.showToast({
title: '获取订单列表失败',
icon: 'none',
})
} finally {
loading.value = false
}
}
//
const onClick = item => {
console.log('点击订单:', item)
@ -222,13 +208,6 @@ const onClick = item => {
url: `/pages/myOrder/orderDetail?id=${item.id}`,
})
}
onUnload(() => {
//
list.value = []
noData.value = false
loading.value = false
})
</script>
<style lang="scss" scoped>