分页与加载更多

View source
客户端自动分页、服务端手动分页与加载状态,以及触底加载、无限滚动的实用参数。

客户端自动分页

传入 v-model:pagination 即触发自动分页,组件在本地切片数据并渲染默认分页栏:

工号姓名部门岗位
P0001
李勇
产品
UI 设计师
P0002
王超
市场
运营专员
P0003
陈娜
设计
全栈工程师
P0004
刘丽
运营
数据分析师
P0005
杨军
研发
后端工程师
<script setup lang="ts">
import type { DataTableColumn, PaginationState } from '@movk/nuxt'
import type { Person } from '~/composables/useTableMock'

const data = makePeople(80)
const pagination = ref<PaginationState>({ pageIndex: 0, pageSize: 5 })
const columns: DataTableColumn<Person>[] = [
  { accessorKey: 'id', header: '工号' },
  { accessorKey: 'name', header: '姓名' },
  { accessorKey: 'department', header: '部门' },
  { accessorKey: 'role', header: '岗位' }
]
</script>

<template>
  <MDataTable
    v-model:pagination="pagination"
    :data="data"
    :columns="columns"
    :ui="{ root: 'max-h-[60vh]' }"
  />
</template>

服务端手动分页

paginationOptions.manualPagination 设为 true 并提供 rowCount,由调用方接管分页——只把当前页数据传给 data。下例用本地切片模拟服务端,实际场景中应在 pagination 变化时请求后端:

工号姓名部门岗位
P0001
李勇
产品
UI 设计师
P0002
王超
市场
运营专员
P0003
陈娜
设计
全栈工程师
P0004
刘丽
运营
数据分析师
P0005
杨军
研发
后端工程师
<script setup lang="ts">
import type { DataTableColumn, PaginationState } from '@movk/nuxt'
import type { Person } from '~/composables/useTableMock'

// 用本地切片模拟服务端:仅持有当前页数据 + 已知总条数 rowCount
const all = makePeople(80)
const pagination = ref<PaginationState>({ pageIndex: 0, pageSize: 5 })
const rowCount = all.length
const pageData = computed(() => {
  const start = pagination.value.pageIndex * pagination.value.pageSize
  return all.slice(start, start + pagination.value.pageSize)
})
const columns: DataTableColumn<Person>[] = [
  { accessorKey: 'id', header: '工号' },
  { accessorKey: 'name', header: '姓名' },
  { accessorKey: 'department', header: '部门' },
  { accessorKey: 'role', header: '岗位' }
]
</script>

<template>
  <MDataTable
    v-model:pagination="pagination"
    :data="pageData"
    :columns="columns"
    :pagination-options="{ manualPagination: true, rowCount }"
    :ui="{ root: 'max-h-[60vh]' }"
  />
</template>
配合 :loading:sorting-options="{ manualSorting: true }"v-model:sorting 可实现服务端排序;搜索时把 pagination.pageIndex 重置为 0。

页数形态

后端只返回总页数而非总条数时,改用 paginationOptions.pageCount 直接指定总页数:

工号姓名部门岗位
P0001
李勇
产品
UI 设计师
P0002
王超
市场
运营专员
P0003
陈娜
设计
全栈工程师
P0004
刘丽
运营
数据分析师
P0005
杨军
研发
后端工程师
<script setup lang="ts">
import type { DataTableColumn, PaginationState } from '@movk/nuxt'
import type { Person } from '~/composables/useTableMock'

// 服务端只返回页数(pageCount),不返回总条数
const all = makePeople(80)
const pagination = ref<PaginationState>({ pageIndex: 0, pageSize: 5 })
const pageCount = computed(() => Math.ceil(all.length / pagination.value.pageSize))
const pageData = computed(() => {
  const start = pagination.value.pageIndex * pagination.value.pageSize
  return all.slice(start, start + pagination.value.pageSize)
})
const columns: DataTableColumn<Person>[] = [
  { accessorKey: 'id', header: '工号' },
  { accessorKey: 'name', header: '姓名' },
  { accessorKey: 'department', header: '部门' },
  { accessorKey: 'role', header: '岗位' }
]
</script>

<template>
  <MDataTable
    v-model:pagination="pagination"
    :data="pageData"
    :columns="columns"
    :pagination-options="{ manualPagination: true, pageCount }"
    :ui="{ root: 'max-h-[60vh]' }"
  />
</template>

每页条数切换

paginationUi.pageSizes 长度大于 1 时显示每页条数切换器:

工号姓名部门岗位
P0001
李勇
产品
UI 设计师
P0002
王超
市场
运营专员
P0003
陈娜
设计
全栈工程师
P0004
刘丽
运营
数据分析师
P0005
杨军
研发
后端工程师
<script setup lang="ts">
import type { DataTableColumn, PaginationState } from '@movk/nuxt'
import type { Person } from '~/composables/useTableMock'

const data = makePeople(40)
const pagination = ref<PaginationState>({ pageIndex: 0, pageSize: 5 })
const columns: DataTableColumn<Person>[] = [
  { accessorKey: 'id', header: '工号' },
  { accessorKey: 'name', header: '姓名' },
  { accessorKey: 'department', header: '部门' },
  { accessorKey: 'role', header: '岗位' }
]
</script>

<template>
  <MDataTable
    v-model:pagination="pagination"
    :data="data"
    :columns="columns"
    :pagination-ui="{ pageSizes: [5, 10, 20, 50] }"
    :ui="{ root: 'max-h-[60vh]' }"
  />
</template>

分页摘要与文案

v-model:row-selection 配合 row-key 联动已选计数;paginationUishowSelectedCountshowRowRangetext 控制摘要区显示与文案:

工号姓名部门岗位
P0001
李勇
产品
UI 设计师
P0002
王超
市场
运营专员
P0003
陈娜
设计
全栈工程师
P0004
刘丽
运营
数据分析师
P0005
杨军
研发
后端工程师
P0006
黄涛
产品
产品经理
P0007
赵伟
市场
前端工程师
P0008
吴敏
设计
UI 设计师
P0009
周强
运营
运营专员
P0010
徐洋
研发
全栈工程师
<script setup lang="ts">
import type { DataTableColumn, PaginationState, RowSelectionState } from '@movk/nuxt'
import type { Person } from '~/composables/useTableMock'

const data = makePeople(40)
const pagination = ref<PaginationState>({ pageIndex: 0, pageSize: 10 })
const selection = ref<RowSelectionState>({})
const columns: DataTableColumn<Person>[] = [
  { accessorKey: 'id', header: '工号' },
  { accessorKey: 'name', header: '姓名' },
  { accessorKey: 'department', header: '部门' },
  { accessorKey: 'role', header: '岗位' }
]
</script>

<template>
  <MDataTable
    v-model:pagination="pagination"
    v-model:row-selection="selection"
    :data="data"
    :columns="columns"
    row-key="id"
    select-on-row-click
    :pagination-ui="{
      showSelectedCount: true,
      showRowRange: true,
      text: { total: '总计', item: '人', range: '当前', selected: '勾选' }
    }"
    :ui="{ root: 'max-h-[60vh]' }"
  />
</template>

透传分页组件

paginationUi.paginationProps 透传 UPaginationshowEdgessiblingCount 等;pageSizeSelectProps 透传每页条数选择器属性:

工号姓名部门岗位
P0001
李勇
产品
UI 设计师
P0002
王超
市场
运营专员
P0003
陈娜
设计
全栈工程师
P0004
刘丽
运营
数据分析师
P0005
杨军
研发
后端工程师
<script setup lang="ts">
import type { DataTableColumn, PaginationState } from '@movk/nuxt'
import type { Person } from '~/composables/useTableMock'

const data = makePeople(80)
const pagination = ref<PaginationState>({ pageIndex: 0, pageSize: 5 })
const columns: DataTableColumn<Person>[] = [
  { accessorKey: 'id', header: '工号' },
  { accessorKey: 'name', header: '姓名' },
  { accessorKey: 'department', header: '部门' },
  { accessorKey: 'role', header: '岗位' }
]
</script>

<template>
  <MDataTable
    v-model:pagination="pagination"
    :data="data"
    :columns="columns"
    :pagination-ui="{
      pageSizes: [5, 10, 20],
      paginationProps: { showEdges: true, siblingCount: 1, color: 'success' },
      pageSizeSelectProps: { size: 'sm', variant: 'subtle' }
    }"
    :ui="{ root: 'max-h-[60vh]' }"
  />
</template>

分页栏槽位

#pagination-summary#pagination-actions 分别替换摘要区与操作区:

工号姓名部门岗位
P0001
李勇
产品
UI 设计师
P0002
王超
市场
运营专员
P0003
陈娜
设计
全栈工程师
P0004
刘丽
运营
数据分析师
P0005
杨军
研发
后端工程师
<script setup lang="ts">
import type { DataTableColumn, PaginationState } from '@movk/nuxt'
import type { Person } from '~/composables/useTableMock'

const data = makePeople(40)
const pagination = ref<PaginationState>({ pageIndex: 0, pageSize: 5 })
const columns: DataTableColumn<Person>[] = [
  { accessorKey: 'id', header: '工号' },
  { accessorKey: 'name', header: '姓名' },
  { accessorKey: 'department', header: '部门' },
  { accessorKey: 'role', header: '岗位' }
]
</script>

<template>
  <MDataTable
    v-model:pagination="pagination"
    :data="data"
    :columns="columns"
    :pagination-ui="{ show: true }"
    :ui="{ root: 'max-h-[60vh]' }"
  >
    <template #pagination-summary="{ page, pageCount, rowCount }">
      <span class="text-sm text-muted">
        第 {{ page }} / {{ pageCount }} 页,共 {{ rowCount }} 条
      </span>
    </template>
    <template #pagination-actions="{ page, pageCount, setPage }">
      <UButton size="xs" variant="ghost" icon="i-lucide-chevron-left" :disabled="page <= 1" @click="setPage(page - 1)" />
      <UButton size="xs" variant="ghost" icon="i-lucide-chevron-right" :disabled="page >= pageCount" @click="setPage(page + 1)" />
    </template>
  </MDataTable>
</template>

#pagination 槽位接收完整分页状态(page/pageCount/from/to/setPage 等),整条替换默认分页栏:

工号姓名部门岗位
P0001
李勇
产品
UI 设计师
P0002
王超
市场
运营专员
P0003
陈娜
设计
全栈工程师
P0004
刘丽
运营
数据分析师
P0005
杨军
研发
后端工程师
<script setup lang="ts">
import type { DataTableColumn, PaginationState } from '@movk/nuxt'
import type { Person } from '~/composables/useTableMock'

const data = makePeople(40)
const pagination = ref<PaginationState>({ pageIndex: 0, pageSize: 5 })
const columns: DataTableColumn<Person>[] = [
  { accessorKey: 'id', header: '工号' },
  { accessorKey: 'name', header: '姓名' },
  { accessorKey: 'department', header: '部门' },
  { accessorKey: 'role', header: '岗位' }
]
</script>

<template>
  <MDataTable
    v-model:pagination="pagination"
    :data="data"
    :columns="columns"
    :ui="{ root: 'max-h-[60vh]' }"
  >
    <template #pagination="{ page, pageCount, from, to, setPage }">
      <div class="flex items-center justify-between rounded-md border border-default px-3 py-2">
        <span class="text-sm text-muted">显示 {{ from }}-{{ to }}</span>
        <div class="flex items-center gap-2">
          <UButton size="xs" variant="soft" :disabled="page <= 1" @click="setPage(page - 1)">
            上一页
          </UButton>
          <span class="text-sm tabular-nums">{{ page }} / {{ pageCount }}</span>
          <UButton size="xs" variant="soft" :disabled="page >= pageCount" @click="setPage(page + 1)">
            下一页
          </UButton>
        </div>
      </div>
    </template>
  </MDataTable>
</template>

loading 加载状态

loading 透传给底层 UTable 显示加载态,适合首屏请求或切页等待:

工号姓名岗位
没有数据
<script setup lang="ts">
import type { DataTableColumn } from '@movk/nuxt'
import type { Person } from '~/composables/useTableMock'

const columns: DataTableColumn<Person>[] = [
  { accessorKey: 'id', header: '工号', size: 100 },
  { accessorKey: 'name', header: '姓名', size: 120 },
  { accessorKey: 'role', header: '岗位', size: 160 }
]
</script>

<template>
  <MDataTable :columns="columns" :data="[]" loading />
</template>

loadMore 触底加载

传入 load-more 回调即启用无限滚动模式(自动隐藏内置分页)。回调返回 Promise 时,组件在加载期间自动派生 loading

已加载 0 / 60
工号姓名部门岗位
没有数据
<script setup lang="ts">
import type { DataTableColumn } from '@movk/nuxt'
import type { Person } from '~/composables/useTableMock'

const PAGE_SIZE = 15
const TOTAL = 60
const items = ref<Person[]>([])
const loaded = ref(0)

const columns: DataTableColumn<Person>[] = [
  { accessorKey: 'id', header: '工号' },
  { accessorKey: 'name', header: '姓名' },
  { accessorKey: 'department', header: '部门' },
  { accessorKey: 'role', header: '岗位' }
]

// 模拟异步分页:每次追加一页,返回 Promise 让组件自动派生 loading
async function loadMore() {
  await new Promise(r => setTimeout(r, 600))
  const next = makePeople(Math.min(PAGE_SIZE, TOTAL - loaded.value), loaded.value)
  items.value = [...items.value, ...next]
  loaded.value += next.length
}

const canLoadMore = computed(() => loaded.value < TOTAL)

function reset() {
  items.value = []
  loaded.value = 0
}
</script>

<template>
  <div class="flex flex-col gap-3">
    <div class="flex items-center gap-3">
      <UButton size="xs" variant="outline" icon="i-lucide-rotate-ccw" @click="reset">
        重置
      </UButton>
      <span class="text-xs text-muted tabular-nums">已加载 {{ loaded }} / {{ TOTAL }}</span>
    </div>
    <MDataTable
      :data="items"
      :columns="columns"
      :load-more="loadMore"
      :can-load-more="canLoadMore"
      :load-more-distance="100"
      :ui="{ root: 'h-[60vh]' }"
    />
  </div>
</template>
需要一个受限高度的滚动容器(如 ui.root: 'h-[60vh]')才能产生滚动并触发触底加载。

canLoadMore 终止加载

can-load-morefalse 时停止触发,可独立于剩余数据强制暂停(如无更多数据、出错后停止):

已加载 0 / 48
工号姓名部门
没有数据
<script setup lang="ts">
import type { DataTableColumn } from '@movk/nuxt'
import type { Person } from '~/composables/useTableMock'

const PAGE_SIZE = 12
const TOTAL = 48
const items = ref<Person[]>([])
const loaded = ref(0)
const paused = ref(false)

const columns: DataTableColumn<Person>[] = [
  { accessorKey: 'id', header: '工号' },
  { accessorKey: 'name', header: '姓名' },
  { accessorKey: 'department', header: '部门' }
]

async function loadMore() {
  await new Promise(r => setTimeout(r, 500))
  const next = makePeople(Math.min(PAGE_SIZE, TOTAL - loaded.value), loaded.value)
  items.value = [...items.value, ...next]
  loaded.value += next.length
}

const canLoadMore = computed(() => !paused.value && loaded.value < TOTAL)

function reset(): void {
  items.value = []
  loaded.value = 0
}
</script>

<template>
  <div class="flex flex-col gap-3">
    <div class="flex items-center gap-3">
      <USwitch v-model="paused" label="暂停加载" />
      <UButton size="xs" variant="outline" icon="i-lucide-rotate-ccw" @click="reset">
        重置
      </UButton>
      <span class="text-xs text-muted tabular-nums">已加载 {{ loaded }} / {{ TOTAL }}</span>
    </div>
    <MDataTable
      :data="items"
      :columns="columns"
      :load-more="loadMore"
      :can-load-more="canLoadMore"
      :ui="{ root: 'h-[50vh]' }"
    />
  </div>
</template>

loadMoreDistance 触发阈值

load-more-distance 设置触发加载的距底像素阈值(默认 100),值越大越提前触发:

已加载 0 / 60
工号姓名部门
没有数据
<script setup lang="ts">
import type { DataTableColumn } from '@movk/nuxt'
import type { Person } from '~/composables/useTableMock'

const PAGE_SIZE = 12
const TOTAL = 60
const items = ref<Person[]>([])
const loaded = ref(0)
const distance = ref(100)

const columns: DataTableColumn<Person>[] = [
  { accessorKey: 'id', header: '工号' },
  { accessorKey: 'name', header: '姓名' },
  { accessorKey: 'department', header: '部门' }
]

async function loadMore() {
  await new Promise(r => setTimeout(r, 500))
  const next = makePeople(Math.min(PAGE_SIZE, TOTAL - loaded.value), loaded.value)
  items.value = [...items.value, ...next]
  loaded.value += next.length
}

const canLoadMore = computed(() => loaded.value < TOTAL)

function reset(): void {
  items.value = []
  loaded.value = 0
}
</script>

<template>
  <div class="flex flex-col gap-3">
    <div class="flex items-center gap-3">
      <USelect
        v-model="distance"
        :items="[
          { label: 'distance 40(贴底触发)', value: 40 },
          { label: 'distance 100(默认)', value: 100 },
          { label: 'distance 240(提前触发)', value: 240 }
        ]"
        value-key="value"
        size="xs"
        class="w-52"
      />
      <UButton size="xs" variant="outline" icon="i-lucide-rotate-ccw" @click="reset">
        重置
      </UButton>
      <span class="text-xs text-muted tabular-nums">已加载 {{ loaded }} / {{ TOTAL }}</span>
    </div>
    <MDataTable
      :data="items"
      :columns="columns"
      :load-more="loadMore"
      :can-load-more="canLoadMore"
      :load-more-distance="distance"
      :ui="{ root: 'h-[50vh]' }"
    />
  </div>
</template>

调用方接管 loading

若希望由调用方接管 loading,可让 load-more 同步返回(不交回 Promise)并自行传入 :loading,与自动派生形态对照:

idle已加载 0 / 48
工号姓名部门
没有数据
<script setup lang="ts">
import type { DataTableColumn } from '@movk/nuxt'
import type { Person } from '~/composables/useTableMock'

const PAGE_SIZE = 12
const TOTAL = 48
const items = ref<Person[]>([])
const loaded = ref(0)
const loading = ref(false)

const columns: DataTableColumn<Person>[] = [
  { accessorKey: 'id', header: '工号' },
  { accessorKey: 'name', header: '姓名' },
  { accessorKey: 'department', header: '部门' }
]

// 同步返回(不交回 Promise),由调用方自行接管 loading
function loadMore(): void {
  if (loading.value) return
  loading.value = true
  setTimeout(() => {
    const next = makePeople(Math.min(PAGE_SIZE, TOTAL - loaded.value), loaded.value)
    items.value = [...items.value, ...next]
    loaded.value += next.length
    loading.value = false
  }, 600)
}

const canLoadMore = computed(() => loaded.value < TOTAL)

function reset(): void {
  items.value = []
  loaded.value = 0
  loading.value = false
}
</script>

<template>
  <div class="flex flex-col gap-3">
    <div class="flex items-center gap-3">
      <UButton size="xs" variant="outline" icon="i-lucide-rotate-ccw" @click="reset">
        重置
      </UButton>
      <UBadge :color="loading ? 'primary' : 'neutral'" variant="subtle" size="sm">
        {{ loading ? 'loading…' : 'idle' }}
      </UBadge>
      <span class="text-xs text-muted tabular-nums">已加载 {{ loaded }} / {{ TOTAL }}</span>
    </div>
    <MDataTable
      :data="items"
      :columns="columns"
      :load-more="loadMore"
      :can-load-more="canLoadMore"
      :loading="loading"
      :ui="{ root: 'h-[50vh]' }"
    />
  </div>
</template>
Copyright © 2025 - 2026 YiXuan - MIT License