Skip to content

ProTable

ProTable targets search and browsing workflows while coordinating local/remote data, search, pagination, sorting, filters, column state, selection, and row editing.

When to use

Use it when a page needs to organize querying, data loading, pagination, column settings, selection, or row editing around a table.

Examples

Request, editing, and slots

View source
vue
<script setup lang="ts">
import { ref } from 'vue'
import {
  ProTable,
  type EditableConfig,
  type ProColumns,
  type ProKey,
  type ProRequest,
  type ProTableInstance,
} from 'antdv-next-pro'

type Project = Record<string, unknown> & {
  id: number
  name: string
  owner: string
  status: 'running' | 'done'
  budget: number
}

type Query = Record<string, unknown> & {
  name?: string
  owner?: string
  status?: Project['status']
  minBudget?: number
}

const projects: Project[] = [
  { id: 1, name: '增长驾驶舱', owner: '林默', status: 'running', budget: 80 },
  { id: 2, name: '会员洞察', owner: '周芮', status: 'done', budget: 45 },
  { id: 3, name: '留存预警', owner: '孟晴', status: 'running', budget: 60 },
  { id: 4, name: '区域经营', owner: '方屿', status: 'done', budget: 35 },
]

const tableRef = ref<ProTableInstance<Project>>()
const visibleRows = ref<Project[]>([])
const editableKeys = ref<ProKey[]>([])
const collapsed = ref(true)
const lastAction = ref('可展开查询区,也可通过 ref 刷新或进入编辑')

const columns: ProColumns<Project>[] = [
  { title: '#', valueType: 'indexBorder', width: 56, search: false },
  {
    title: '项目',
    dataIndex: 'name',
    valueType: 'text',
    formItemProps: { rules: [{ required: true, message: '请输入项目名称' }] },
  },
  { title: '负责人', dataIndex: 'owner', valueType: 'text' },
  {
    title: '状态',
    dataIndex: 'status',
    valueType: 'select',
    editable: false,
    valueEnum: {
      running: { text: '进行中', status: 'processing' },
      done: { text: '已完成', status: 'success' },
    },
  },
  {
    title: '最低预算',
    dataIndex: 'budget',
    valueType: 'money',
    search: { transform: (value) => ({ minBudget: value }) },
  },
]

const request: ProRequest<Project, Query> = async (params) => {
  await new Promise((resolve) => setTimeout(resolve, 180))
  const name = String(params.name ?? '').toLowerCase()
  const owner = String(params.owner ?? '').toLowerCase()
  const data = projects.filter(
    (item) =>
      (!name || item.name.toLowerCase().includes(name)) &&
      (!owner || item.owner.toLowerCase().includes(owner)) &&
      (!params.status || item.status === params.status) &&
      (!params.minBudget || item.budget >= Number(params.minBudget)),
  )
  return { data, total: data.length, success: true }
}

const editable: EditableConfig<Project> = {
  type: 'multiple',
  async onSave(_key, record) {
    await new Promise((resolve) => setTimeout(resolve, 120))
    lastAction.value = `已保存「${record.name}」`
  },
}

const editFirst = () => {
  const first = visibleRows.value[0]
  if (first && tableRef.value?.startEditable(first.id)) {
    lastAction.value = `正在编辑「${first.name}」`
  }
}

const reload = async () => {
  await tableRef.value?.reload()
  lastAction.value = '已通过组件 ref 重新请求'
}
</script>

<template>
  <div class="demo-frame vp-raw">
    <p class="demo-label">LIVE · REQUEST + EDITABLE + SLOTS</p>
    <div class="demo-actions">
      <span>{{ lastAction }}</span>
      <button type="button" @click="editFirst">编辑第一行</button>
    </div>
    <ProTable
      ref="tableRef"
      v-model:data-source="visibleRows"
      v-model:editable-keys="editableKeys"
      :columns="columns"
      :request="request"
      :editable="editable"
      row-key="id"
      :pagination="false"
      :row-selection="{}"
      :search="{ collapsed, span: 8, labelWidth: 'auto' }"
      :options="{ reload: true, setting: true }"
      @search-collapse="collapsed = $event"
      @request-error="lastAction = '请求失败,现有数据已保留'"
    >
      <template #toolbar-title> 项目清单 · {{ visibleRows.length }} 条 </template>
      <template #toolbar-actions>
        <button class="slot-button" type="button" @click="reload">ref 刷新</button>
      </template>
      <template #cell-status="{ value }">
        <span :class="['status-chip', `is-${value}`]">
          {{ value === 'running' ? '进行中' : '已完成' }}
        </span>
      </template>
    </ProTable>
  </div>
</template>

<style scoped>
.demo-actions {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 14px;
  color: #64748b;
  font-size: 13px;
}

.demo-actions button,
.slot-button {
  padding: 5px 10px;
  border: 1px solid #bfd2e7;
  border-radius: 6px;
  background: #fff;
  color: #1768d3;
  cursor: pointer;
}

.status-chip {
  display: inline-flex;
  padding: 2px 8px;
  border-radius: 999px;
  font-size: 12px;
}

.status-chip.is-running {
  background: #e6f7f7;
  color: #087b84;
}

.status-chip.is-done {
  background: #eef5ff;
  color: #1768d3;
}
</style>

Data modes

Pass dataSource or defaultDataSource for local mode. Search, sorting, filters, and pagination operate on the local rows:

vue
<ProTable
  v-model:data-source="rows"
  :columns="columns"
  row-key="id"
  :pagination="{ defaultPageSize: 20 }"
/>

Remote mode uses one fixed request contract:

ts
import type { ProRequest } from 'antdv-next-pro'

const request: ProRequest<User, Query> = async (params, sort, filter) => {
  const result = await api.list({ ...params, sort, filter })
  return {
    data: result.items,
    total: result.total,
    success: true,
  }
}

Search, pagination, sorting, filters, and external params all feed this request. Only the most recently started concurrent request may update the table. A thrown error preserves current rows, ends loading, and emits request-error. A result with success: false is ignored.

manualRequest skips the initial request; call reload() through the component ref later. postData synchronously transforms successful data before display.

Search and collapse

search: false disables the search area. A column with dataIndex becomes searchable unless hideInSearch or search: false is set. search.transform can map one field to different request parameters.

ts
const columns = [
  {
    title: 'Minimum score',
    dataIndex: 'score',
    valueType: 'digit',
    search: {
      transform: (value) => ({ minScore: value }),
    },
  },
]

Use defaultCollapsed for uncontrolled initial state:

vue
<ProTable :search="{ defaultCollapsed: true, span: 8, labelWidth: 'auto' }" />

Use collapsed and search-collapse for controlled state:

vue
<ProTable
  :search="{ collapsed, span: 8, searchText: 'Filter', resetText: 'Clear' }"
  @search-collapse="collapsed = $event"
/>

span defaults to 8, or three fields in a 24-column row. Collapsed mode renders the first row only. search.onCollapse(next) is also available.

Column state and settings

The built-in column settings panel in the toolbar only toggles visibility. Control column order and fixed placement programmatically with order and fixed under each column key in columnsState (the column key takes precedence, otherwise dataIndex is used):

ts
const columnsState = ref<Record<string, ProColumnsState>>({
  name: { show: true, order: 10, fixed: 'left' },
  status: { show: true, order: 20 },
  actions: { show: true, order: 30, fixed: 'right' },
})

const tableColumnsState = computed<ProColumnsStateConfig>(() => ({
  value: columnsState.value,
  onChange: (next) => {
    columnsState.value = next
  },
  persistenceKey: 'users-table-columns',
  persistenceType: 'localStorage',
}))
vue
<ProTable :columns-state="tableColumnsState" />

Pair value with onChange for fully controlled state, or use defaultValue for an uncontrolled initial state. With persistenceKey, the state can be stored in localStorage or sessionStorage, including show changes made by the panel and programmatic order / fixed values.

Built-in editing

ProTable uses the same editing state machine as EditableProTable:

vue
<ProTable
  v-model:data-source="rows"
  v-model:editable-keys="editableKeys"
  :columns="columns"
  :editable="{
    type: 'multiple',
    onSave: saveRow,
    onCancel: cancelRow,
    onDelete: deleteRow,
  }"
/>

Column editable can vary by row. formItemProps.rules provides sync/async validation. A custom renderFormItem(column, context) editor writes through context.update(nextValue), updating the shared editing state and EditableProTable's live v-model:value. ProTable adds an action column when no valueType: 'option' column exists.

editable.actionRender(record, actions) replaces the complete default action area. Check actions.editing, call actions.start() for a read-only row, and use save(), cancel(), or remove() while editing.

addEditRecord(record, { position, parentKey, newRecordType }) supports top/bottom insertion, tree parents, and cache/dataSource creator modes. Every new record must have a unique rowKey.

Shared field core

Search fields, editable cells, and standalone ProFormFields use the same field registry, option-request behavior, and readonly formatting:

  • Search items use form-item mode. name / label generated from column dataIndex / title override the same keys in formItemProps.
  • Editable cells use the bare-control behavior behind fieldMode="field". Column formItemProps.rules still perform validation without creating a nested FormItem.
  • Async options for select, treeSelect, radio, checkbox, and segmented only accept the newest request. Empty remote results are valid; failures call onFieldRequestError(error) and preserve current options.

valueType now includes treeSelect, slider, and segmented. Captcha and both Upload fields are standalone ProFormFields and have no column valueType mapping.

Continue using renderFormItem(column, context) for a fully custom search field or editor, and a column slot or render for custom readonly cells. Their priority and context.update(value) protocol are unchanged.

API

Props

PropTypeDescription
columnsProColumns<T>[]Shared table, search, and editor description
dataSourceT[]Controlled local rows; supports v-model:data-source
defaultDataSourceT[]Uncontrolled initial rows
requestProRequest<T, P>Remote Promise request
paramsPExtra params; changes reset the page and reload
postData(data: T[]) => T[]Synchronous display transform
rowKeykeyof T | string | (record) => ProKeyUnique row identity, defaults to id
loadingbooleanExternal loading state
searchfalse | ProTableSearchConfigSearch and collapse options
paginationfalse | ProTablePaginationPage state and options
optionsfalse | ProTableOptionsDensity, fullscreen, reload, settings
toolbarfalse | { title?, actions? }Toolbar content; slots are also available
rowSelectionfalse | Record<string, unknown>Antdv Next row-selection config
columnsStateProColumnsStateConfigVisibility, order, fixed-state, and persistence config
editablefalse | EditableConfig<T>Single/multiple row editing lifecycle
editableKeysProKey[]Editing keys; supports v-model:editable-keys
pollingnumberPoll interval in milliseconds; pauses when hidden
revalidateOnFocusbooleanReload on window focus
manualRequestbooleanSkip the initial automatic request
scroll / size / borderedMatching Antdv Next valuesScrolling, density, and borders

Events

EventArgumentsDescription
update:data-sourcerowsv-model:data-source update
update:editable-keyskeysv-model:editable-keys update
data-source-changerows, changedRecord?Edit, creator, or delete mutation
request-errorerrorRemote request failure
editable-errorerrorSave/delete lifecycle failure
validation-errorkey, errorsRow validation failure
search-collapsecollapsedSearch collapse change
changepagination, filters, sorterPage, filter, or sort change
selection-changekeys, rowsRow selection change
loadrows, totalAccepted remote result

Slots

The column key is column.key or the dot-joined dataIndex.

vue
<ProTable :columns="columns">
  <template #toolbar-title>Projects</template>
  <template #toolbar-actions>
    <button @click="tableRef?.reload()">Sync</button>
  </template>
  <template #header-name="{ column }">
    {{ column.title }} · custom header
  </template>
  <template #cell-name="{ value, editable }">
    <strong>{{ value }}</strong>
    <small v-if="editable">editing</small>
  </template>
</ProTable>
  • header-${columnKey} receives { column }.
  • cell-${columnKey} or ${columnKey} receives { value, record, index, column, editable }.
  • Other slots are forwarded to the underlying Antdv Next Table.

A cell slot takes precedence over the default display and editor. Prefer a header-only slot for editable columns unless the slot handles editing itself.

Component ref

ts
import type { ProTableInstance } from 'antdv-next-pro'

const tableRef = ref<ProTableInstance<User>>()

await tableRef.value?.reload(true)
tableRef.value?.setPageInfo({ current: 2, pageSize: 50 })
tableRef.value?.clearSelected()
tableRef.value?.startEditable(userId)
await tableRef.value?.saveEditable(userId)
MethodDescription
reload(resetPageIndex?)Reload, optionally from page one
reset()Clear search/sort/filters and restore initial pagination
setPageInfo(page)Set current page or page size
clearSelected()Clear row selection
fullScreen()Enter or exit fullscreen
scrollTo(target){ key } scrolls to a row key, { top } scrolls by pixels; strings are keys
startEditable(key)Start editing
saveEditable(key)Validate and save; returns success
cancelEditable(key)Cancel and restore the original row
addEditRecord(record, options?)Create a row and enter edit mode

Released under the MIT License