Pagination & Load More

View source
Client-side auto pagination, server-side manual pagination with loading state, and bottom-trigger load-more for infinite scroll.

Client-Side Auto Pagination

Passing v-model:pagination triggers automatic client-side pagination; the component slices the data locally and renders the default pagination bar:

工号姓名部门岗位
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>

Server-Side Manual Pagination

Set paginationOptions.manualPagination to true and provide rowCount to hand off pagination to the caller — pass only the current page's data via data. The example below uses local slicing to simulate a server; in a real scenario you should request the backend whenever pagination changes:

工号姓名部门岗位
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>
Combine :loading, :sorting-options="{ manualSorting: true }" and v-model:sorting for server-side sorting; reset pagination.pageIndex to 0 when a search is triggered.

Page-Count Mode

When the backend returns only the total page count rather than total rows, use paginationOptions.pageCount to specify the total pages directly:

工号姓名部门岗位
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>

Page Size Switching

When paginationUi.pageSizes has more than one item, a page-size switcher is shown:

工号姓名部门岗位
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>

Pagination Summary and Labels

v-model:row-selection combined with row-key syncs the selected count; paginationUi's showSelectedCount, showRowRange and text control the summary area's display and labels:

工号姓名部门岗位
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>

Passing Props to the Pagination Component

paginationUi.paginationProps passes through UPagination props such as showEdges and siblingCount; pageSizeSelectProps passes through page-size selector props:

工号姓名部门岗位
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 Slots

#pagination-summary and #pagination-actions replace the summary area and actions area respectively:

工号姓名部门岗位
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>

The #pagination slot receives the full pagination state (page, pageCount, from, to, setPage, etc.) and replaces the entire default pagination bar:

工号姓名部门岗位
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 State

loading is passed through to the underlying UTable to display a loading indicator, suitable for initial data fetch or page-turn waits:

工号姓名岗位
No data
<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 Bottom-Triggered Load

Passing a load-more callback enables infinite scroll mode (the built-in pagination bar is automatically hidden). When the callback returns a Promise, the component automatically derives loading during the load:

已加载 0 / 60
工号姓名部门岗位
No data
<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>
A scroll container with a constrained height (e.g. ui.root: 'h-[60vh]') is required to produce scrolling and trigger the bottom-load callback.

canLoadMore Stop Loading

When can-load-more is false, triggering stops — you can force a pause independently of remaining data (e.g. no more data, or stop after an error):

已加载 0 / 48
工号姓名部门
No data
<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 Trigger Threshold

load-more-distance sets the distance-from-bottom threshold in pixels (default 100) — a larger value triggers loading earlier:

已加载 0 / 60
工号姓名部门
No data
<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>

Caller-Managed Loading

If you want the caller to manage loading state, make load-more return synchronously (without handing back a Promise) and pass :loading manually, in contrast to the auto-derived form:

idle已加载 0 / 48
工号姓名部门
No data
<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