Skip to content

Pagination 分页组件

基于Element Plus的增强分页组件,提供更丰富的功能和更好的用户体验。

📋 基础用法

简单分页

vue
<template>
  <div>
    <!-- 数据表格 -->
    <el-table :data="tableData" border>
      <el-table-column prop="id" label="ID" width="80" />
      <el-table-column prop="name" label="姓名" />
      <el-table-column prop="email" label="邮箱" />
      <el-table-column prop="createTime" label="创建时间" />
    </el-table>

    <!-- 分页组件 -->
    <Pagination
      v-model:current="queryParams.pageNum"
      v-model:size="queryParams.pageSize"
      :total="total"
      @change="handlePageChange"
    />
  </div>
</template>

<script setup lang="ts">
import Pagination from '@/components/Pagination/index.vue'

const queryParams = reactive({
  pageNum: 1,
  pageSize: 10
})

const total = ref(0)
const tableData = ref([])

const handlePageChange = () => {
  console.log('页码变化:', queryParams.pageNum, queryParams.pageSize)
  // 重新获取数据
  fetchData()
}

const fetchData = async () => {
  // 模拟API调用
  console.log('获取数据...', queryParams)
}
</script>

完整功能分页

vue
<template>
  <div>
    <el-table :data="tableData" border>
      <el-table-column prop="id" label="ID" />
      <el-table-column prop="name" label="姓名" />
      <el-table-column prop="status" label="状态" />
    </el-table>

    <Pagination
      v-model:current="pagination.current"
      v-model:size="pagination.size"
      :total="pagination.total"
      :show-size-changer="true"
      :show-quick-jumper="true"
      :show-total="true"
      :page-sizes="[10, 20, 50, 100]"
      :background="true"
      layout="total, sizes, prev, pager, next, jumper"
      @change="handleChange"
      @size-change="handleSizeChange"
    />
  </div>
</template>

<script setup lang="ts">
const pagination = reactive({
  current: 1,
  size: 10,
  total: 0
})

const tableData = ref([])

const handleChange = (page: number) => {
  console.log('页码变化:', page)
  fetchData()
}

const handleSizeChange = (size: number) => {
  console.log('页面大小变化:', size)
  pagination.current = 1 // 重置到第一页
  fetchData()
}

const fetchData = async () => {
  // API调用逻辑
}
</script>

🎯 组件实现

Pagination 组件

vue
<!-- components/Pagination/index.vue -->
<template>
  <div
    v-if="total > 0"
    class="pagination-container"
    :class="{ 'hidden': hidden }"
  >
    <el-pagination
      v-model:current-page="currentPage"
      v-model:page-size="pageSize"
      :total="total"
      :page-sizes="pageSizes"
      :layout="layout"
      :background="background"
      :small="small"
      :disabled="disabled"
      :hide-on-single-page="hideOnSinglePage"
      :pager-count="pagerCount"
      @current-change="handleCurrentChange"
      @size-change="handleSizeChange"
    />

    <!-- 额外信息 -->
    <div v-if="showInfo" class="pagination-info">
      <span class="info-text">
        共 {{ total }} 条记录,每页显示 {{ pageSize }} 条
      </span>
      <span v-if="showRange" class="range-text">
        显示第 {{ rangeStart }} - {{ rangeEnd }} 条记录
      </span>
    </div>

    <!-- 快速跳转 -->
    <div v-if="showQuickJumper && !layout.includes('jumper')" class="quick-jumper">
      <span>跳至</span>
      <el-input-number
        v-model="jumpPage"
        :min="1"
        :max="totalPages"
        :controls="false"
        size="small"
        style="width: 80px"
        @keyup.enter="handleJump"
      />
      <span>页</span>
      <el-button size="small" @click="handleJump">跳转</el-button>
    </div>
  </div>
</template>

<script setup lang="ts">
interface Props {
  // v-model 绑定
  current?: number
  size?: number

  // 基础配置
  total: number
  pageSizes?: number[]
  layout?: string
  background?: boolean
  small?: boolean
  disabled?: boolean
  hidden?: boolean

  // 显示配置
  hideOnSinglePage?: boolean
  pagerCount?: number
  showInfo?: boolean
  showRange?: boolean
  showQuickJumper?: boolean

  // 自定义配置
  showSizeChanger?: boolean
  showTotal?: boolean
}

interface Emits {
  (e: 'update:current', value: number): void
  (e: 'update:size', value: number): void
  (e: 'change', current: number, size: number): void
  (e: 'current-change', current: number): void
  (e: 'size-change', size: number): void
}

const props = withDefaults(defineProps<Props>(), {
  current: 1,
  size: 10,
  pageSizes: () => [10, 20, 50, 100],
  layout: 'total, sizes, prev, pager, next, jumper',
  background: true,
  small: false,
  disabled: false,
  hidden: false,
  hideOnSinglePage: false,
  pagerCount: 7,
  showInfo: false,
  showRange: false,
  showQuickJumper: false,
  showSizeChanger: true,
  showTotal: true
})

const emit = defineEmits<Emits>()

// 双向绑定
const currentPage = computed({
  get: () => props.current,
  set: (value) => emit('update:current', value)
})

const pageSize = computed({
  get: () => props.size,
  set: (value) => emit('update:size', value)
})

// 计算属性
const totalPages = computed(() => Math.ceil(props.total / props.size))

const rangeStart = computed(() => {
  return (props.current - 1) * props.size + 1
})

const rangeEnd = computed(() => {
  return Math.min(props.current * props.size, props.total)
})

// 快速跳转
const jumpPage = ref(props.current)

// 处理页码变化
const handleCurrentChange = (page: number) => {
  emit('update:current', page)
  emit('current-change', page)
  emit('change', page, props.size)
}

// 处理页面大小变化
const handleSizeChange = (size: number) => {
  // 计算新的页码,保持当前数据位置尽量不变
  const newCurrent = Math.min(
    Math.ceil(((props.current - 1) * props.size + 1) / size),
    Math.ceil(props.total / size)
  )

  emit('update:size', size)
  emit('update:current', newCurrent)
  emit('size-change', size)
  emit('change', newCurrent, size)
}

// 快速跳转
const handleJump = () => {
  if (jumpPage.value >= 1 && jumpPage.value <= totalPages.value) {
    handleCurrentChange(jumpPage.value)
  }
}

// 监听当前页变化,同步快速跳转输入框
watch(() => props.current, (newCurrent) => {
  jumpPage.value = newCurrent
})
</script>

🔧 高级功能

分页状态管理

typescript
// composables/use-pagination-state.ts
export interface PaginationState {
  current: number
  size: number
  total: number
  showSizeChanger: boolean
  showQuickJumper: boolean
  pageSizes: number[]
}

export function usePaginationState(
  initialState?: Partial<PaginationState>
) {
  const state = reactive<PaginationState>({
    current: 1,
    size: 10,
    total: 0,
    showSizeChanger: true,
    showQuickJumper: true,
    pageSizes: [10, 20, 50, 100],
    ...initialState
  })

  // 计算属性
  const totalPages = computed(() => Math.ceil(state.total / state.size))
  const offset = computed(() => (state.current - 1) * state.size)
  const hasNext = computed(() => state.current < totalPages.value)
  const hasPrev = computed(() => state.current > 1)

  // 方法
  const setTotal = (total: number) => {
    state.total = total
    // 如果当前页超出范围,调整到最后一页
    if (state.current > totalPages.value && totalPages.value > 0) {
      state.current = totalPages.value
    }
  }

  const goToPage = (page: number) => {
    if (page >= 1 && page <= totalPages.value) {
      state.current = page
      return true
    }
    return false
  }

  const nextPage = () => {
    if (hasNext.value) {
      state.current++
      return true
    }
    return false
  }

  const prevPage = () => {
    if (hasPrev.value) {
      state.current--
      return true
    }
    return false
  }

  const changeSize = (size: number) => {
    // 保持当前数据位置尽量不变
    const currentOffset = offset.value
    state.size = size
    state.current = Math.floor(currentOffset / size) + 1
  }

  const reset = () => {
    state.current = 1
    state.total = 0
  }

  const getRange = () => {
    const start = offset.value + 1
    const end = Math.min(offset.value + state.size, state.total)
    return { start, end }
  }

  return {
    state: readonly(state),
    totalPages,
    offset,
    hasNext,
    hasPrev,
    setTotal,
    goToPage,
    nextPage,
    prevPage,
    changeSize,
    reset,
    getRange
  }
}

分页数据管理

typescript
// composables/use-paginated-data.ts
export interface PaginatedData<T> {
  records: T[]
  total: number
  current: number
  size: number
  pages: number
}

export function usePaginatedData<T>(
  fetchFn: (params: { current: number; size: number; [key: string]: any }) => Promise<PaginatedData<T>>,
  options: {
    immediate?: boolean
    defaultParams?: Record<string, any>
    onSuccess?: (data: PaginatedData<T>) => void
    onError?: (error: any) => void
  } = {}
) {
  const { immediate = true, defaultParams = {}, onSuccess, onError } = options

  const { state: pagination, setTotal, reset } = usePaginationState()
  const data = ref<T[]>([])
  const loading = ref(false)
  const error = ref<any>(null)

  // 获取数据
  const fetchData = async (extraParams: Record<string, any> = {}) => {
    loading.value = true
    error.value = null

    try {
      const params = {
        current: pagination.current,
        size: pagination.size,
        ...defaultParams,
        ...extraParams
      }

      const result = await fetchFn(params)

      data.value = result.records
      setTotal(result.total)

      onSuccess?.(result)
    } catch (err) {
      error.value = err
      onError?.(err)
      console.error('获取分页数据失败:', err)
    } finally {
      loading.value = false
    }
  }

  // 刷新当前页
  const refresh = () => {
    fetchData()
  }

  // 重置并重新获取
  const reload = () => {
    reset()
    fetchData()
  }

  // 页码变化处理
  const handlePageChange = (current: number, size: number) => {
    pagination.current = current
    pagination.size = size
    fetchData()
  }

  // 搜索(重置到第一页)
  const search = (searchParams: Record<string, any> = {}) => {
    pagination.current = 1
    fetchData(searchParams)
  }

  // 立即执行
  if (immediate) {
    onMounted(() => {
      fetchData()
    })
  }

  return {
    // 数据状态
    data: readonly(data),
    loading: readonly(loading),
    error: readonly(error),
    pagination,

    // 方法
    fetchData,
    refresh,
    reload,
    handlePageChange,
    search
  }
}

虚拟分页

typescript
// composables/use-virtual-pagination.ts
export function useVirtualPagination<T>(
  allData: Ref<T[]>,
  pageSize = 10
) {
  const currentPage = ref(1)

  // 计算属性
  const total = computed(() => allData.value.length)
  const totalPages = computed(() => Math.ceil(total.value / pageSize))

  const currentData = computed(() => {
    const start = (currentPage.value - 1) * pageSize
    const end = start + pageSize
    return allData.value.slice(start, end)
  })

  const pagination = computed(() => ({
    current: currentPage.value,
    size: pageSize,
    total: total.value,
    pages: totalPages.value
  }))

  // 方法
  const goToPage = (page: number) => {
    if (page >= 1 && page <= totalPages.value) {
      currentPage.value = page
    }
  }

  const nextPage = () => {
    if (currentPage.value < totalPages.value) {
      currentPage.value++
    }
  }

  const prevPage = () => {
    if (currentPage.value > 1) {
      currentPage.value--
    }
  }

  const reset = () => {
    currentPage.value = 1
  }

  return {
    currentData,
    pagination,
    goToPage,
    nextPage,
    prevPage,
    reset
  }
}

📱 移动端适配

响应式分页组件

vue
<!-- components/ResponsivePagination/index.vue -->
<template>
  <div class="responsive-pagination">
    <!-- 桌面端分页 -->
    <el-pagination
      v-if="!isMobile"
      v-model:current-page="currentPage"
      v-model:page-size="pageSize"
      :total="total"
      :page-sizes="pageSizes"
      :layout="layout"
      :background="background"
      @current-change="handleCurrentChange"
      @size-change="handleSizeChange"
    />

    <!-- 移动端分页 -->
    <div v-else class="mobile-pagination">
      <!-- 页码信息 -->
      <div class="page-info">
        <span>第 {{ currentPage }} 页,共 {{ totalPages }} 页</span>
        <span>({{ total }} 条记录)</span>
      </div>

      <!-- 简化控制 -->
      <div class="page-controls">
        <el-button
          :disabled="currentPage <= 1"
          @click="goToPrev"
        >
          上一页
        </el-button>

        <!-- 页码选择器 -->
        <el-select
          v-model="currentPage"
          size="small"
          style="width: 80px"
          @change="handleCurrentChange"
        >
          <el-option
            v-for="page in totalPages"
            :key="page"
            :label="page"
            :value="page"
          />
        </el-select>

        <el-button
          :disabled="currentPage >= totalPages"
          @click="goToNext"
        >
          下一页
        </el-button>
      </div>

      <!-- 每页条数选择 -->
      <div class="size-selector">
        <span>每页</span>
        <el-select
          v-model="pageSize"
          size="small"
          style="width: 80px"
          @change="handleSizeChange"
        >
          <el-option
            v-for="size in pageSizes"
            :key="size"
            :label="size"
            :value="size"
          />
        </el-select>
        <span>条</span>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { useBreakpoints } from '@vueuse/core'

const props = defineProps<{
  current: number
  size: number
  total: number
  pageSizes?: number[]
  layout?: string
  background?: boolean
}>()

const emit = defineEmits<{
  'update:current': [value: number]
  'update:size': [value: number]
  'change': [current: number, size: number]
}>()

// 响应式断点
const breakpoints = useBreakpoints({
  mobile: 768
})

const isMobile = breakpoints.smaller('mobile')

// 双向绑定
const currentPage = computed({
  get: () => props.current,
  set: (value) => emit('update:current', value)
})

const pageSize = computed({
  get: () => props.size,
  set: (value) => emit('update:size', value)
})

// 计算属性
const totalPages = computed(() => Math.ceil(props.total / props.size))

// 方法
const handleCurrentChange = (page: number) => {
  emit('update:current', page)
  emit('change', page, props.size)
}

const handleSizeChange = (size: number) => {
  emit('update:size', size)
  emit('change', props.current, size)
}

const goToPrev = () => {
  if (currentPage.value > 1) {
    handleCurrentChange(currentPage.value - 1)
  }
}

const goToNext = () => {
  if (currentPage.value < totalPages.value) {
    handleCurrentChange(currentPage.value + 1)
  }
}
</script>

Pagination组件为Vue3应用提供了完整的分页解决方案,支持桌面端和移动端的不同展示方式,并提供了丰富的配置选项和数据管理功能。

🎨 样式定制

主题样式

scss
// styles/components/pagination.scss
.pagination-container {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  padding: 16px 0;
  background: var(--el-bg-color);

  // 浮动样式
  &.is-float {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    padding: 12px 24px;
    box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
    z-index: 100;
  }

  // 紧凑模式
  &.is-compact {
    padding: 8px 0;

    :deep(.el-pagination) {
      .el-pager li {
        min-width: 28px;
        height: 28px;
        line-height: 28px;
      }

      .btn-prev,
      .btn-next {
        width: 28px;
        height: 28px;
      }
    }
  }

  // 居中对齐
  &.is-center {
    justify-content: center;
  }

  // 两端对齐
  &.is-between {
    justify-content: space-between;
  }
}

// Element Plus 分页组件样式覆盖
:deep(.el-pagination) {
  // 背景按钮
  &.is-background {
    .el-pager li {
      background-color: var(--el-fill-color-light);
      border-radius: 4px;
      margin: 0 3px;

      &:hover {
        background-color: var(--el-color-primary-light-9);
      }

      &.is-active {
        background-color: var(--el-color-primary);
        color: #fff;
      }
    }
  }

  // 前后按钮
  .btn-prev,
  .btn-next {
    border-radius: 4px;
    background-color: var(--el-fill-color-light);

    &:hover:not(:disabled) {
      background-color: var(--el-color-primary-light-9);
      color: var(--el-color-primary);
    }

    &:disabled {
      color: var(--el-text-color-placeholder);
      cursor: not-allowed;
    }
  }

  // 每页条数选择器
  .el-pagination__sizes {
    .el-select {
      width: 110px;
    }
  }

  // 页码跳转
  .el-pagination__jump {
    .el-input__inner {
      text-align: center;
    }
  }

  // 总数显示
  .el-pagination__total {
    color: var(--el-text-color-regular);
  }
}

// 暗黑模式
.dark {
  .pagination-container {
    background: var(--el-bg-color);

    &.is-float {
      box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.3);
    }
  }

  :deep(.el-pagination) {
    &.is-background {
      .el-pager li {
        background-color: var(--el-fill-color);

        &:hover {
          background-color: var(--el-color-primary-light-7);
        }
      }
    }

    .btn-prev,
    .btn-next {
      background-color: var(--el-fill-color);
    }
  }
}

自定义分页布局

vue
<template>
  <div class="custom-pagination">
    <!-- 左侧信息区 -->
    <div class="pagination-left">
      <slot name="left">
        <span class="total-info">
          共 <strong>{{ total }}</strong> 条记录
        </span>
      </slot>
    </div>

    <!-- 中间分页区 -->
    <div class="pagination-center">
      <el-pagination
        v-model:current-page="currentPage"
        v-model:page-size="pageSize"
        :total="total"
        :page-sizes="pageSizes"
        :layout="layout"
        :background="background"
        @current-change="handleCurrentChange"
        @size-change="handleSizeChange"
      />
    </div>

    <!-- 右侧操作区 -->
    <div class="pagination-right">
      <slot name="right">
        <el-button-group size="small">
          <el-button @click="handleRefresh">
            <el-icon><Refresh /></el-icon>
          </el-button>
          <el-button @click="handleFirst" :disabled="currentPage === 1">
            首页
          </el-button>
          <el-button @click="handleLast" :disabled="currentPage === totalPages">
            末页
          </el-button>
        </el-button-group>
      </slot>
    </div>
  </div>
</template>

<script setup lang="ts">
import { Refresh } from '@element-plus/icons-vue'

interface Props {
  current: number
  size: number
  total: number
  pageSizes?: number[]
  layout?: string
  background?: boolean
}

const props = withDefaults(defineProps<Props>(), {
  pageSizes: () => [10, 20, 50, 100],
  layout: 'prev, pager, next',
  background: true
})

const emit = defineEmits<{
  'update:current': [value: number]
  'update:size': [value: number]
  'change': [current: number, size: number]
  'refresh': []
}>()

const currentPage = computed({
  get: () => props.current,
  set: (value) => emit('update:current', value)
})

const pageSize = computed({
  get: () => props.size,
  set: (value) => emit('update:size', value)
})

const totalPages = computed(() => Math.ceil(props.total / props.size))

const handleCurrentChange = (page: number) => {
  emit('change', page, props.size)
}

const handleSizeChange = (size: number) => {
  emit('change', 1, size)
}

const handleRefresh = () => {
  emit('refresh')
}

const handleFirst = () => {
  if (currentPage.value !== 1) {
    currentPage.value = 1
    handleCurrentChange(1)
  }
}

const handleLast = () => {
  if (currentPage.value !== totalPages.value) {
    currentPage.value = totalPages.value
    handleCurrentChange(totalPages.value)
  }
}
</script>

<style lang="scss" scoped>
.custom-pagination {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 16px 0;
  gap: 16px;
}

.pagination-left {
  .total-info {
    color: var(--el-text-color-regular);

    strong {
      color: var(--el-color-primary);
      margin: 0 4px;
    }
  }
}

.pagination-center {
  flex: 1;
  display: flex;
  justify-content: center;
}

.pagination-right {
  display: flex;
  align-items: center;
  gap: 8px;
}
</style>

🔗 与表格组件集成

表格分页封装

vue
<!-- components/TableWithPagination/index.vue -->
<template>
  <div class="table-with-pagination">
    <!-- 工具栏 -->
    <div v-if="$slots.toolbar" class="table-toolbar">
      <slot name="toolbar" />
    </div>

    <!-- 表格 -->
    <el-table
      ref="tableRef"
      v-loading="loading"
      :data="data"
      :border="border"
      :stripe="stripe"
      :height="height"
      :max-height="maxHeight"
      :row-key="rowKey"
      :highlight-current-row="highlightCurrentRow"
      @selection-change="handleSelectionChange"
      @sort-change="handleSortChange"
    >
      <slot />
    </el-table>

    <!-- 分页 -->
    <Pagination
      v-if="showPagination"
      v-model:current="paginationState.current"
      v-model:size="paginationState.size"
      :total="paginationState.total"
      :page-sizes="pageSizes"
      :layout="paginationLayout"
      :background="true"
      class="table-pagination"
      @change="handlePageChange"
    />
  </div>
</template>

<script setup lang="ts">
import type { TableInstance } from 'element-plus'
import Pagination from '@/components/Pagination/index.vue'

interface Props {
  data: any[]
  loading?: boolean
  border?: boolean
  stripe?: boolean
  height?: string | number
  maxHeight?: string | number
  rowKey?: string | ((row: any) => string)
  highlightCurrentRow?: boolean
  showPagination?: boolean
  pageSizes?: number[]
  paginationLayout?: string
  paginationState: {
    current: number
    size: number
    total: number
  }
}

const props = withDefaults(defineProps<Props>(), {
  loading: false,
  border: true,
  stripe: true,
  highlightCurrentRow: true,
  showPagination: true,
  pageSizes: () => [10, 20, 50, 100],
  paginationLayout: 'total, sizes, prev, pager, next, jumper'
})

const emit = defineEmits<{
  'page-change': [current: number, size: number]
  'selection-change': [selection: any[]]
  'sort-change': [{ column: any; prop: string; order: string | null }]
}>()

const tableRef = ref<TableInstance>()

// 分页变化
const handlePageChange = (current: number, size: number) => {
  emit('page-change', current, size)
}

// 选择变化
const handleSelectionChange = (selection: any[]) => {
  emit('selection-change', selection)
}

// 排序变化
const handleSortChange = ({ column, prop, order }: any) => {
  emit('sort-change', { column, prop, order })
}

// 暴露表格方法
defineExpose({
  tableRef,
  clearSelection: () => tableRef.value?.clearSelection(),
  toggleRowSelection: (row: any, selected?: boolean) => {
    tableRef.value?.toggleRowSelection(row, selected)
  },
  setCurrentRow: (row: any) => tableRef.value?.setCurrentRow(row),
  clearSort: () => tableRef.value?.clearSort(),
  sort: (prop: string, order: string) => tableRef.value?.sort(prop, order)
})
</script>

<style lang="scss" scoped>
.table-with-pagination {
  display: flex;
  flex-direction: column;
  height: 100%;
}

.table-toolbar {
  margin-bottom: 16px;
}

.el-table {
  flex: 1;
}

.table-pagination {
  margin-top: 16px;
  flex-shrink: 0;
}
</style>

完整使用示例

vue
<template>
  <div class="user-management">
    <TableWithPagination
      :data="tableData"
      :loading="loading"
      :pagination-state="pagination"
      @page-change="handlePageChange"
      @selection-change="handleSelectionChange"
    >
      <!-- 工具栏 -->
      <template #toolbar>
        <div class="toolbar">
          <el-form :model="queryForm" inline>
            <el-form-item label="用户名">
              <el-input
                v-model="queryForm.username"
                placeholder="请输入用户名"
                clearable
                @keyup.enter="handleSearch"
              />
            </el-form-item>
            <el-form-item label="状态">
              <el-select v-model="queryForm.status" placeholder="请选择" clearable>
                <el-option label="启用" value="1" />
                <el-option label="禁用" value="0" />
              </el-select>
            </el-form-item>
            <el-form-item>
              <el-button type="primary" @click="handleSearch">搜索</el-button>
              <el-button @click="handleReset">重置</el-button>
            </el-form-item>
          </el-form>

          <div class="toolbar-actions">
            <el-button type="primary" @click="handleAdd">
              <el-icon><Plus /></el-icon>新增
            </el-button>
            <el-button
              type="danger"
              :disabled="selectedRows.length === 0"
              @click="handleBatchDelete"
            >
              批量删除
            </el-button>
          </div>
        </div>
      </template>

      <!-- 表格列 -->
      <el-table-column type="selection" width="55" />
      <el-table-column prop="id" label="ID" width="80" sortable />
      <el-table-column prop="username" label="用户名" min-width="120" />
      <el-table-column prop="email" label="邮箱" min-width="180" />
      <el-table-column prop="status" label="状态" width="100">
        <template #default="{ row }">
          <el-tag :type="row.status === '1' ? 'success' : 'danger'">
            {{ row.status === '1' ? '启用' : '禁用' }}
          </el-tag>
        </template>
      </el-table-column>
      <el-table-column prop="createTime" label="创建时间" width="180" sortable />
      <el-table-column label="操作" width="200" fixed="right">
        <template #default="{ row }">
          <el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
          <el-button type="danger" link @click="handleDelete(row)">删除</el-button>
        </template>
      </el-table-column>
    </TableWithPagination>
  </div>
</template>

<script setup lang="ts">
import { Plus } from '@element-plus/icons-vue'
import TableWithPagination from '@/components/TableWithPagination/index.vue'
import { getUserListApi } from '@/api/system/user'

// 查询表单
const queryForm = reactive({
  username: '',
  status: ''
})

// 分页状态
const pagination = reactive({
  current: 1,
  size: 10,
  total: 0
})

// 表格数据
const tableData = ref([])
const loading = ref(false)
const selectedRows = ref<any[]>([])

// 获取数据
const fetchData = async () => {
  loading.value = true
  try {
    const { data } = await getUserListApi({
      pageNum: pagination.current,
      pageSize: pagination.size,
      ...queryForm
    })
    tableData.value = data.records
    pagination.total = data.total
  } catch (error) {
    console.error('获取用户列表失败:', error)
  } finally {
    loading.value = false
  }
}

// 分页变化
const handlePageChange = (current: number, size: number) => {
  pagination.current = current
  pagination.size = size
  fetchData()
}

// 选择变化
const handleSelectionChange = (selection: any[]) => {
  selectedRows.value = selection
}

// 搜索
const handleSearch = () => {
  pagination.current = 1
  fetchData()
}

// 重置
const handleReset = () => {
  queryForm.username = ''
  queryForm.status = ''
  pagination.current = 1
  fetchData()
}

// 初始化
onMounted(() => {
  fetchData()
})
</script>

❓ 常见问题

1. 分页与路由同步失败

问题描述

在列表页面使用分页后,用户刷新页面或通过浏览器后退返回时,分页状态丢失,总是显示第一页。

问题原因

  • 分页状态仅保存在组件内部响应式数据中
  • 页面刷新时组件重新初始化,状态丢失
  • 未将分页参数同步到 URL 查询字符串

解决方案

typescript
// composables/use-route-pagination.ts
import { useRoute, useRouter } from 'vue-router'

export function useRoutePagination(defaultSize = 10) {
  const route = useRoute()
  const router = useRouter()

  // 从 URL 读取分页参数
  const pagination = reactive({
    current: Number(route.query.page) || 1,
    size: Number(route.query.size) || defaultSize,
    total: 0
  })

  // 同步分页参数到 URL
  const syncToRoute = () => {
    router.replace({
      query: {
        ...route.query,
        page: pagination.current.toString(),
        size: pagination.size.toString()
      }
    })
  }

  // 页码变化处理
  const handlePageChange = (current: number, size: number) => {
    pagination.current = current
    pagination.size = size
    syncToRoute()
  }

  // 监听路由变化,更新分页状态
  watch(
    () => route.query,
    (query) => {
      const page = Number(query.page)
      const size = Number(query.size)

      if (page && page !== pagination.current) {
        pagination.current = page
      }
      if (size && size !== pagination.size) {
        pagination.size = size
      }
    },
    { immediate: true }
  )

  return {
    pagination,
    handlePageChange,
    syncToRoute
  }
}
vue
<!-- 使用示例 -->
<template>
  <div>
    <el-table :data="tableData" border>
      <!-- 表格列 -->
    </el-table>

    <Pagination
      v-model:current="pagination.current"
      v-model:size="pagination.size"
      :total="pagination.total"
      @change="handlePageChange"
    />
  </div>
</template>

<script setup lang="ts">
import { useRoutePagination } from '@/composables/use-route-pagination'

const { pagination, handlePageChange } = useRoutePagination()

const tableData = ref([])

// 监听分页变化自动获取数据
watch(
  () => [pagination.current, pagination.size],
  () => {
    fetchData()
  },
  { immediate: true }
)

const fetchData = async () => {
  const { data } = await getListApi({
    pageNum: pagination.current,
    pageSize: pagination.size
  })
  tableData.value = data.records
  pagination.total = data.total
}
</script>

2. 删除最后一页最后一条数据后页面空白

问题描述

在列表最后一页只有一条数据时,删除该数据后页面显示空白,没有自动跳转到上一页。

问题原因

  • 删除数据后未检查当前页是否还有数据
  • 删除后直接刷新当前页,但该页已不存在数据
  • 未处理边界情况:当前页 > 总页数

解决方案

typescript
// composables/use-smart-pagination.ts
export function useSmartPagination() {
  const pagination = reactive({
    current: 1,
    size: 10,
    total: 0
  })

  // 计算总页数
  const totalPages = computed(() => Math.ceil(pagination.total / pagination.size))

  // 删除后智能调整页码
  const adjustAfterDelete = (deletedCount = 1) => {
    const newTotal = pagination.total - deletedCount

    if (newTotal <= 0) {
      // 所有数据都被删除
      pagination.current = 1
      pagination.total = 0
      return
    }

    pagination.total = newTotal
    const newTotalPages = Math.ceil(newTotal / pagination.size)

    // 如果当前页超出范围,跳转到最后一页
    if (pagination.current > newTotalPages) {
      pagination.current = newTotalPages
    }
  }

  // 批量删除后调整
  const adjustAfterBatchDelete = (deletedIds: any[], currentPageIds: any[]) => {
    // 计算当前页被删除的数量
    const deletedInCurrentPage = deletedIds.filter(id =>
      currentPageIds.includes(id)
    ).length

    const remainingInCurrentPage = currentPageIds.length - deletedInCurrentPage

    // 更新总数
    pagination.total -= deletedIds.length

    // 如果当前页数据全部被删除且不是第一页
    if (remainingInCurrentPage === 0 && pagination.current > 1) {
      pagination.current--
    }

    // 确保当前页不超出范围
    const newTotalPages = Math.ceil(pagination.total / pagination.size)
    if (pagination.current > newTotalPages && newTotalPages > 0) {
      pagination.current = newTotalPages
    }
  }

  return {
    pagination,
    totalPages,
    adjustAfterDelete,
    adjustAfterBatchDelete
  }
}
vue
<script setup lang="ts">
import { useSmartPagination } from '@/composables/use-smart-pagination'
import { ElMessage, ElMessageBox } from 'element-plus'

const { pagination, adjustAfterDelete, adjustAfterBatchDelete } = useSmartPagination()
const tableData = ref([])

// 删除单条
const handleDelete = async (row: any) => {
  await ElMessageBox.confirm('确定要删除该记录吗?', '提示', {
    type: 'warning'
  })

  await deleteApi(row.id)
  ElMessage.success('删除成功')

  // 智能调整页码
  adjustAfterDelete(1)

  // 重新获取数据
  fetchData()
}

// 批量删除
const handleBatchDelete = async () => {
  if (selectedRows.value.length === 0) {
    ElMessage.warning('请选择要删除的记录')
    return
  }

  await ElMessageBox.confirm(
    `确定要删除选中的 ${selectedRows.value.length} 条记录吗?`,
    '提示',
    { type: 'warning' }
  )

  const deletedIds = selectedRows.value.map(row => row.id)
  const currentPageIds = tableData.value.map(row => row.id)

  await batchDeleteApi(deletedIds)
  ElMessage.success('删除成功')

  // 智能调整页码
  adjustAfterBatchDelete(deletedIds, currentPageIds)

  // 清空选择
  selectedRows.value = []

  // 重新获取数据
  fetchData()
}
</script>

3. 切换每页条数后当前页不正确

问题描述

用户在第 5 页查看数据,切换每页条数从 10 条变为 50 条后,当前页码仍为 5,但实际上总页数已经减少,导致显示空白或跳转错误。

问题原因

  • 切换每页条数时未重新计算当前页码
  • 没有考虑用户当前查看的数据行在新分页下的位置
  • 直接保持原页码导致超出范围

解决方案

typescript
// composables/use-pagination-size-change.ts
export function usePaginationSizeChange() {
  const pagination = reactive({
    current: 1,
    size: 10,
    total: 0
  })

  // 智能切换每页条数
  const handleSizeChange = (newSize: number) => {
    // 计算当前查看的第一条数据索引
    const currentFirstIndex = (pagination.current - 1) * pagination.size

    // 计算新的页码(保持查看位置)
    const newCurrent = Math.floor(currentFirstIndex / newSize) + 1

    // 确保不超出范围
    const newTotalPages = Math.ceil(pagination.total / newSize)
    pagination.current = Math.min(newCurrent, Math.max(1, newTotalPages))
    pagination.size = newSize
  }

  // 或者简单策略:切换每页条数后回到第一页
  const handleSizeChangeSimple = (newSize: number) => {
    pagination.size = newSize
    pagination.current = 1
  }

  return {
    pagination,
    handleSizeChange,
    handleSizeChangeSimple
  }
}
vue
<template>
  <Pagination
    v-model:current="pagination.current"
    v-model:size="pagination.size"
    :total="pagination.total"
    @size-change="handleSizeChange"
    @current-change="handleCurrentChange"
  />
</template>

<script setup lang="ts">
const pagination = reactive({
  current: 1,
  size: 10,
  total: 100
})

// 每页条数变化 - 智能计算新页码
const handleSizeChange = (newSize: number) => {
  // 当前查看的数据范围起始位置
  const startIndex = (pagination.current - 1) * pagination.size

  // 计算在新页面大小下的页码
  const newCurrent = Math.floor(startIndex / newSize) + 1

  // 更新分页
  pagination.size = newSize
  pagination.current = Math.max(1, Math.min(newCurrent, Math.ceil(pagination.total / newSize)))

  // 获取数据
  fetchData()
}

// 页码变化
const handleCurrentChange = (current: number) => {
  pagination.current = current
  fetchData()
}
</script>

4. 分页组件在表格数据为空时仍然显示

问题描述

表格没有数据时,分页组件仍然显示,显示"共 0 条"和不可用的分页按钮,影响用户体验。

问题原因

  • 未根据数据总数控制分页组件的显示
  • 使用了默认的 hide-on-single-page 属性但不适用于零数据情况
  • 加载状态和空数据状态未区分处理

解决方案

vue
<template>
  <div class="list-container">
    <el-table :data="tableData" v-loading="loading" border>
      <!-- 表格列 -->
      <template #empty>
        <el-empty description="暂无数据" />
      </template>
    </el-table>

    <!-- 方案1:使用 v-if 控制显示 -->
    <Pagination
      v-if="!loading && pagination.total > 0"
      v-model:current="pagination.current"
      v-model:size="pagination.size"
      :total="pagination.total"
      @change="handlePageChange"
    />

    <!-- 方案2:组件内部处理 -->
    <SmartPagination
      v-model:current="pagination.current"
      v-model:size="pagination.size"
      :total="pagination.total"
      :loading="loading"
      :show-when-empty="false"
      @change="handlePageChange"
    />
  </div>
</template>
vue
<!-- components/SmartPagination/index.vue -->
<template>
  <transition name="fade">
    <div
      v-if="shouldShow"
      class="smart-pagination"
      :class="{ 'is-loading': loading }"
    >
      <el-pagination
        v-model:current-page="currentPage"
        v-model:page-size="pageSize"
        :total="total"
        :page-sizes="pageSizes"
        :layout="layout"
        :background="true"
        :disabled="loading"
        @current-change="handleCurrentChange"
        @size-change="handleSizeChange"
      />
    </div>
  </transition>
</template>

<script setup lang="ts">
interface Props {
  current: number
  size: number
  total: number
  loading?: boolean
  showWhenEmpty?: boolean
  minTotal?: number
  pageSizes?: number[]
  layout?: string
}

const props = withDefaults(defineProps<Props>(), {
  loading: false,
  showWhenEmpty: false,
  minTotal: 1,
  pageSizes: () => [10, 20, 50, 100],
  layout: 'total, sizes, prev, pager, next, jumper'
})

const emit = defineEmits<{
  'update:current': [value: number]
  'update:size': [value: number]
  'change': [current: number, size: number]
}>()

// 是否显示分页
const shouldShow = computed(() => {
  // 加载中时保持之前的显示状态
  if (props.loading) return props.total > 0

  // 根据配置决定空数据时是否显示
  if (props.showWhenEmpty) return true

  // 数据量达到最小阈值才显示
  return props.total >= props.minTotal
})

const currentPage = computed({
  get: () => props.current,
  set: (value) => emit('update:current', value)
})

const pageSize = computed({
  get: () => props.size,
  set: (value) => emit('update:size', value)
})

const handleCurrentChange = (page: number) => {
  emit('change', page, props.size)
}

const handleSizeChange = (size: number) => {
  emit('update:current', 1)
  emit('change', 1, size)
}
</script>

<style lang="scss" scoped>
.smart-pagination {
  padding: 16px 0;
  display: flex;
  justify-content: flex-end;

  &.is-loading {
    opacity: 0.6;
    pointer-events: none;
  }
}

.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.3s ease;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}
</style>

5. 分页状态在多标签页间共享导致冲突

问题描述

使用 Keep-alive 缓存的列表页面,在多个标签页中打开同一列表时,分页状态相互影响,导致数据混乱。

问题原因

  • 多个组件实例共享同一个 Pinia Store 或全局状态
  • 路由切换时未正确恢复各自的分页状态
  • Keep-alive 缓存导致组件状态被保留

解决方案

typescript
// composables/use-scoped-pagination.ts
const paginationMap = new Map<string, any>()

export function useScopedPagination(scopeKey: string, defaultSize = 10) {
  // 获取或创建该作用域的分页状态
  const getOrCreateState = () => {
    if (!paginationMap.has(scopeKey)) {
      paginationMap.set(scopeKey, reactive({
        current: 1,
        size: defaultSize,
        total: 0
      }))
    }
    return paginationMap.get(scopeKey)
  }

  const pagination = getOrCreateState()

  // 重置该作用域的分页
  const reset = () => {
    pagination.current = 1
    pagination.total = 0
  }

  // 销毁该作用域的分页状态
  const destroy = () => {
    paginationMap.delete(scopeKey)
  }

  return {
    pagination,
    reset,
    destroy
  }
}
vue
<!-- 在组件中使用 -->
<script setup lang="ts">
import { useScopedPagination } from '@/composables/use-scoped-pagination'

// 使用路由路径作为作用域 key
const route = useRoute()
const scopeKey = computed(() => route.fullPath)

const { pagination, reset, destroy } = useScopedPagination(scopeKey.value)

// 监听路由变化,重新获取对应的分页状态
watch(scopeKey, (newKey) => {
  const { pagination: newPagination } = useScopedPagination(newKey)
  Object.assign(pagination, newPagination)
})

// 组件卸载时清理(可选,取决于是否需要缓存)
onUnmounted(() => {
  // 如果不需要缓存分页状态,取消注释下行
  // destroy()
})
</script>
typescript
// 另一种方案:使用路由 meta 存储分页状态
// router/index.ts
const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: '/user/list',
      component: () => import('@/views/user/list.vue'),
      meta: {
        keepAlive: true,
        paginationState: null // 用于存储分页状态
      }
    }
  ]
})

// 在组件中使用
const route = useRoute()

const pagination = computed({
  get: () => route.meta.paginationState || { current: 1, size: 10, total: 0 },
  set: (val) => { route.meta.paginationState = val }
})

6. 快速连续点击分页导致数据错乱

问题描述

用户快速连续点击不同页码时,由于网络请求的异步性,后发的请求可能先返回,导致显示的数据与当前页码不匹配。

问题原因

  • 每次点击都发起新请求,但未取消之前的请求
  • 请求返回顺序不确定
  • 未使用请求标识来验证返回数据的有效性

解决方案

typescript
// composables/use-safe-pagination.ts
export function useSafePagination<T>(
  fetchFn: (params: { current: number; size: number }) => Promise<{ records: T[]; total: number }>
) {
  const pagination = reactive({
    current: 1,
    size: 10,
    total: 0
  })

  const data = ref<T[]>([])
  const loading = ref(false)

  // 请求标识
  let requestId = 0
  let abortController: AbortController | null = null

  const fetchData = async () => {
    // 取消之前的请求
    if (abortController) {
      abortController.abort()
    }

    // 创建新的取消控制器
    abortController = new AbortController()

    // 记录当前请求 ID
    const currentRequestId = ++requestId

    loading.value = true

    try {
      const result = await fetchFn({
        current: pagination.current,
        size: pagination.size
      })

      // 验证请求是否仍然有效
      if (currentRequestId !== requestId) {
        console.log('请求已过期,忽略结果')
        return
      }

      data.value = result.records
      pagination.total = result.total
    } catch (error) {
      // 忽略取消的请求错误
      if (error instanceof DOMException && error.name === 'AbortError') {
        return
      }
      throw error
    } finally {
      // 只有最新请求才更新 loading 状态
      if (currentRequestId === requestId) {
        loading.value = false
      }
    }
  }

  // 防抖处理页码变化
  const debouncedFetch = useDebounceFn(fetchData, 150)

  const handlePageChange = (current: number, size: number) => {
    pagination.current = current
    pagination.size = size
    debouncedFetch()
  }

  // 清理
  onUnmounted(() => {
    if (abortController) {
      abortController.abort()
    }
  })

  return {
    data: readonly(data),
    pagination,
    loading: readonly(loading),
    fetchData,
    handlePageChange
  }
}
vue
<template>
  <div>
    <el-table :data="data" v-loading="loading" border>
      <!-- 表格列 -->
    </el-table>

    <Pagination
      v-model:current="pagination.current"
      v-model:size="pagination.size"
      :total="pagination.total"
      :disabled="loading"
      @change="handlePageChange"
    />
  </div>
</template>

<script setup lang="ts">
import { useSafePagination } from '@/composables/use-safe-pagination'
import { getUserListApi } from '@/api/system/user'

const {
  data,
  pagination,
  loading,
  fetchData,
  handlePageChange
} = useSafePagination(async (params) => {
  const response = await getUserListApi({
    pageNum: params.current,
    pageSize: params.size
  })
  return response.data
})

onMounted(() => {
  fetchData()
})
</script>

7. 分页组件性能问题导致大量数据卡顿

问题描述

当总数据量很大(如 10 万条以上)时,分页组件计算页码和渲染变得缓慢,每次点击分页都有明显延迟。

问题原因

  • 生成过多的页码选项(如下拉跳转列表)
  • 计算属性触发过于频繁
  • 未对大数据量场景做优化处理

解决方案

vue
<!-- components/LargePagination/index.vue -->
<template>
  <div class="large-pagination">
    <!-- 页码信息 -->
    <div class="pagination-info">
      <span>第 {{ current }} / {{ displayTotalPages }} 页</span>
      <span class="total-text">共 {{ formattedTotal }} 条</span>
    </div>

    <!-- 简化分页控制 -->
    <div class="pagination-controls">
      <el-button
        :disabled="current <= 1"
        @click="goToFirst"
      >
        首页
      </el-button>
      <el-button
        :disabled="current <= 1"
        @click="goToPrev"
      >
        上一页
      </el-button>

      <!-- 页码输入 -->
      <div class="page-input">
        <el-input-number
          v-model="inputPage"
          :min="1"
          :max="totalPages"
          :controls="false"
          size="default"
          @keyup.enter="goToInputPage"
        />
        <el-button @click="goToInputPage">跳转</el-button>
      </div>

      <el-button
        :disabled="current >= totalPages"
        @click="goToNext"
      >
        下一页
      </el-button>
      <el-button
        :disabled="current >= totalPages"
        @click="goToLast"
      >
        末页
      </el-button>
    </div>

    <!-- 每页条数选择 -->
    <div class="size-selector">
      <span>每页</span>
      <el-select v-model="innerSize" @change="handleSizeChange">
        <el-option
          v-for="s in pageSizes"
          :key="s"
          :label="s"
          :value="s"
        />
      </el-select>
      <span>条</span>
    </div>
  </div>
</template>

<script setup lang="ts">
interface Props {
  current: number
  size: number
  total: number
  pageSizes?: number[]
  maxDisplayPages?: number
}

const props = withDefaults(defineProps<Props>(), {
  pageSizes: () => [20, 50, 100, 200],
  maxDisplayPages: 9999
})

const emit = defineEmits<{
  'update:current': [value: number]
  'update:size': [value: number]
  'change': [current: number, size: number]
}>()

// 计算属性 - 使用 shallowRef 减少响应式开销
const totalPages = computed(() => Math.ceil(props.total / props.size))

// 显示的总页数(避免显示过大数字)
const displayTotalPages = computed(() => {
  if (totalPages.value > props.maxDisplayPages) {
    return `${props.maxDisplayPages}+`
  }
  return totalPages.value.toString()
})

// 格式化总数
const formattedTotal = computed(() => {
  if (props.total >= 10000) {
    return `${(props.total / 10000).toFixed(1)}万`
  }
  return props.total.toLocaleString()
})

const inputPage = ref(props.current)
const innerSize = ref(props.size)

// 监听外部变化
watch(() => props.current, (val) => {
  inputPage.value = val
})

watch(() => props.size, (val) => {
  innerSize.value = val
})

// 页面导航方法
const goToFirst = () => {
  emit('update:current', 1)
  emit('change', 1, props.size)
}

const goToPrev = () => {
  const newPage = Math.max(1, props.current - 1)
  emit('update:current', newPage)
  emit('change', newPage, props.size)
}

const goToNext = () => {
  const newPage = Math.min(totalPages.value, props.current + 1)
  emit('update:current', newPage)
  emit('change', newPage, props.size)
}

const goToLast = () => {
  emit('update:current', totalPages.value)
  emit('change', totalPages.value, props.size)
}

const goToInputPage = () => {
  const page = Math.max(1, Math.min(inputPage.value, totalPages.value))
  inputPage.value = page
  emit('update:current', page)
  emit('change', page, props.size)
}

const handleSizeChange = (size: number) => {
  emit('update:size', size)
  emit('update:current', 1)
  emit('change', 1, size)
}
</script>

<style lang="scss" scoped>
.large-pagination {
  display: flex;
  align-items: center;
  gap: 24px;
  padding: 16px 0;
  flex-wrap: wrap;
}

.pagination-info {
  display: flex;
  align-items: center;
  gap: 12px;
  color: var(--el-text-color-regular);

  .total-text {
    color: var(--el-text-color-secondary);
  }
}

.pagination-controls {
  display: flex;
  align-items: center;
  gap: 8px;
}

.page-input {
  display: flex;
  align-items: center;
  gap: 8px;

  .el-input-number {
    width: 80px;
  }
}

.size-selector {
  display: flex;
  align-items: center;
  gap: 8px;

  .el-select {
    width: 100px;
  }
}
</style>

8. 服务端分页参数格式不兼容

问题描述

后端接口使用不同的分页参数命名(如 page/pageNo/pageNumlimit/pageSize/size),需要频繁转换参数格式。

问题原因

  • 不同后端服务使用不同的分页参数规范
  • 前后端未统一分页参数命名
  • 缺乏统一的参数转换层

解决方案

typescript
// utils/pagination-adapter.ts

// 定义不同的分页参数格式
export interface StandardPagination {
  current: number
  size: number
  total?: number
}

export interface PageNumFormat {
  pageNum: number
  pageSize: number
  total?: number
}

export interface PageFormat {
  page: number
  limit: number
  total?: number
}

export interface OffsetFormat {
  offset: number
  limit: number
  total?: number
}

// 分页适配器
export class PaginationAdapter {
  // 标准格式转 pageNum/pageSize
  static toPageNum(pagination: StandardPagination): PageNumFormat {
    return {
      pageNum: pagination.current,
      pageSize: pagination.size,
      total: pagination.total
    }
  }

  // 标准格式转 page/limit
  static toPageLimit(pagination: StandardPagination): PageFormat {
    return {
      page: pagination.current,
      limit: pagination.size,
      total: pagination.total
    }
  }

  // 标准格式转 offset/limit
  static toOffset(pagination: StandardPagination): OffsetFormat {
    return {
      offset: (pagination.current - 1) * pagination.size,
      limit: pagination.size,
      total: pagination.total
    }
  }

  // pageNum/pageSize 转标准格式
  static fromPageNum(data: PageNumFormat): StandardPagination {
    return {
      current: data.pageNum,
      size: data.pageSize,
      total: data.total
    }
  }

  // page/limit 转标准格式
  static fromPageLimit(data: PageFormat): StandardPagination {
    return {
      current: data.page,
      size: data.limit,
      total: data.total
    }
  }

  // offset/limit 转标准格式
  static fromOffset(data: OffsetFormat, limit: number): StandardPagination {
    return {
      current: Math.floor(data.offset / limit) + 1,
      size: limit,
      total: data.total
    }
  }
}

// 创建适配后的请求函数
export function createPaginatedRequest<T, P extends StandardPagination>(
  requestFn: (params: any) => Promise<any>,
  format: 'pageNum' | 'pageLimit' | 'offset' = 'pageNum'
) {
  return async (pagination: P, extraParams?: Record<string, any>) => {
    let paginationParams: any

    switch (format) {
      case 'pageNum':
        paginationParams = PaginationAdapter.toPageNum(pagination)
        break
      case 'pageLimit':
        paginationParams = PaginationAdapter.toPageLimit(pagination)
        break
      case 'offset':
        paginationParams = PaginationAdapter.toOffset(pagination)
        break
    }

    const response = await requestFn({
      ...paginationParams,
      ...extraParams
    })

    return {
      records: response.data?.records || response.data?.list || [],
      total: response.data?.total || 0
    }
  }
}
typescript
// api/user.ts
import { createPaginatedRequest } from '@/utils/pagination-adapter'
import request from '@/utils/request'

// 用户列表接口使用 pageNum/pageSize 格式
export const getUserList = createPaginatedRequest(
  (params) => request.get('/api/user/list', { params }),
  'pageNum'
)

// 订单列表接口使用 page/limit 格式
export const getOrderList = createPaginatedRequest(
  (params) => request.get('/api/order/list', { params }),
  'pageLimit'
)

// 日志列表接口使用 offset/limit 格式
export const getLogList = createPaginatedRequest(
  (params) => request.get('/api/log/list', { params }),
  'offset'
)
vue
<!-- 使用示例 -->
<script setup lang="ts">
import { getUserList } from '@/api/user'

const pagination = reactive({
  current: 1,
  size: 10,
  total: 0
})

const tableData = ref([])

const fetchData = async () => {
  const { records, total } = await getUserList(
    pagination,
    { status: '1' } // 额外参数
  )
  tableData.value = records
  pagination.total = total
}
</script>