Special Columns

View source
Configuration for selection, index, row-pinning and actions columns — including tree selection strategies, dynamic button props and confirmation dialogs.

Overview

Data columns bind fields via accessorKey; special columns are distinguished by type and do not need an accessorKey:

typePurpose
selectionRow selection (single/multiple, tree cascade strategies)
indexRow index
expandTree expansion (see Tree Data)
row-pinningPin rows to top or bottom
actionsRow action buttons
All special columns also support visibility, pinable, resizable, fixed, size, align.

Selection Column

mode Single / Multiple

How mode works: the default multiple automatically renders header and cell checkboxes and syncs the selected-id dictionary via v-model:row-selection; single disables multi-select, shows a custom header, and keeps at most one selected row at all times:

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

const props = defineProps<{
  mode: 'multiple' | 'single'
}>()

const data = makePeople(6)
const selection = ref<RowSelectionState>({})

const columns = computed<DataTableColumn<Person>[]>(() => [
  props.mode === 'single'
    ? { type: 'selection', mode: 'single', header: '选择', size: 80 }
    : { type: 'selection' },
  { accessorKey: 'name', header: '姓名' },
  { accessorKey: 'department', header: '部门' },
  { accessorKey: 'role', header: '岗位' }
])
</script>

<template>
  <div class="flex flex-col gap-3">
    <MDataTable v-model:row-selection="selection" row-key="id" :columns="columns" :data="data" />
    <pre class="text-xs p-3 rounded-md bg-muted overflow-auto">rowSelection: {{ selection }}</pre>
  </div>
</template>

strategy Tree Selection Strategy

strategy takes effect in tree mode (children-key) and controls the parent–child check relationship: cascade for parent–child cascade, isolated for independent selection, leaf for leaf-only selection (parent nodes show a derived state):

cascade(父子级联)
姓名部门
李勇
产品
王超
市场
陈娜
设计
isolated(独立勾选)
姓名部门
李勇
产品
王超
市场
陈娜
设计
leaf(仅叶子)
姓名部门
李勇
产品
王超
市场
陈娜
设计
<script setup lang="ts">
import type { DataTableColumn, DataTableTreeSelectionStrategy, RowSelectionState } from '@movk/nuxt'
import type { Person } from '~/composables/useTableMock'

const treeData = makePeopleTree(3, 3)
const cascade = ref<RowSelectionState>({})
const isolated = ref<RowSelectionState>({})
const leaf = ref<RowSelectionState>({})

const treeBase: DataTableColumn<Person>[] = [
  { type: 'expand' },
  { accessorKey: 'name', header: '姓名' },
  { accessorKey: 'department', header: '部门' }
]
function strategyColumns(strategy: DataTableTreeSelectionStrategy): DataTableColumn<Person>[] {
  return [{ type: 'selection', strategy }, ...treeBase]
}
</script>

<template>
  <div class="grid grid-cols-1 md:grid-cols-3 gap-3">
    <div class="flex flex-col gap-2">
      <div class="text-xs text-muted">
        cascade(父子级联)
      </div>
      <MDataTable
        v-model:row-selection="cascade"
        row-key="id"
        children-key="children"
        :columns="strategyColumns('cascade')"
        :data="treeData"
      />
    </div>
    <div class="flex flex-col gap-2">
      <div class="text-xs text-muted">
        isolated(独立勾选)
      </div>
      <MDataTable
        v-model:row-selection="isolated"
        row-key="id"
        children-key="children"
        :columns="strategyColumns('isolated')"
        :data="treeData"
      />
    </div>
    <div class="flex flex-col gap-2">
      <div class="text-xs text-muted">
        leaf(仅叶子)
      </div>
      <MDataTable
        v-model:row-selection="leaf"
        row-key="id"
        children-key="children"
        :columns="strategyColumns('leaf')"
        :data="treeData"
      />
    </div>
  </div>
</template>

checkboxProps Functional Configuration

checkboxProps receives context and returns props: you can distinguish header/cell by scope, parent row derived state by isLeafAggregate, and derive disabled from row data:

姓名状态
李勇
leave
王超
offboarded
陈娜
active
<script setup lang="ts">
import type { DataTableColumn, RowSelectionState } from '@movk/nuxt'
import type { Person } from '~/composables/useTableMock'

const treeData = makePeopleTree(3, 3)
const selection = ref<RowSelectionState>({
  P0101: true,
  P0102: true,
  P0201: true,
  P0202: true,
  P0203: true
})

const columns: DataTableColumn<Person>[] = [
  {
    type: 'selection',
    strategy: 'leaf',
    checkboxProps: (ctx) => {
      if (ctx.scope === 'header') return { color: 'success' }
      if (ctx.isLeafAggregate) return { color: 'neutral' }
      return {
        disabled: ctx.cellContext.row.original.status === 'offboarded',
        color: 'primary'
      }
    }
  },
  { type: 'expand' },
  { accessorKey: 'name', header: '姓名' },
  { accessorKey: 'status', header: '状态' }
]
</script>

<template>
  <MDataTable
    v-model:row-selection="selection"
    row-key="id"
    children-key="children"
    :columns="columns"
    :data="treeData"
  />
</template>

rowSelectionKeys and treeSelection

v-model:row-selection-keys provides external controlled state in array form; the component-exposed treeSelection derives business views such as leaves, parents, halfSelected and strictlyChecked (accessible via useTemplateRef):

姓名部门
李勇
产品
王超
市场
陈娜
设计
treeSelection: {
  "leaves": [],
  "parents": [],
  "halfSelected": []
}
<script setup lang="ts">
import type { DataTableColumn, DataTableExposed } from '@movk/nuxt'
import type { Person } from '~/composables/useTableMock'

const treeData = makePeopleTree(3, 3)
const selectionKeys = ref<string[]>([])
const tableRef = useTemplateRef<DataTableExposed<Person>>('tableRef')

const derived = computed(() => {
  const r = tableRef.value?.treeSelection
  return {
    leaves: r?.leaves.map(p => p.name) ?? [],
    parents: r?.parents.map(p => p.name) ?? [],
    halfSelected: r?.halfSelected.map(p => p.name) ?? []
  }
})

const columns: DataTableColumn<Person>[] = [
  { type: 'selection' },
  { type: 'expand' },
  { accessorKey: 'name', header: '姓名' },
  { accessorKey: 'department', header: '部门' }
]
</script>

<template>
  <div class="flex flex-col gap-3">
    <MDataTable
      ref="tableRef"
      v-model:row-selection-keys="selectionKeys"
      row-key="id"
      children-key="children"
      :columns="columns"
      :data="treeData"
    />
    <pre class="text-xs p-3 rounded-md bg-muted overflow-auto">treeSelection: {{ derived }}</pre>
  </div>
</template>

Index Column

type: 'index' renders a # header by default; the row number is automatically recalculated with sorting and pagination, and can be customized via header:

姓名
部门
薪资
1
李勇
产品
8257
2
王超
市场
8514
3
陈娜
设计
8771
4
刘丽
运营
9028
5
杨军
研发
9285
<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>[] = [
  { type: 'index', header: '' },
  { accessorKey: 'name', header: '姓名', sortable: true },
  { accessorKey: 'department', header: '部门' },
  { accessorKey: 'salary', header: '薪资', align: 'right', sortable: true }
]
</script>

<template>
  <MDataTable v-model:pagination="pagination" row-key="id" :columns="columns" :data="data" sortable />
</template>

Row Pinning Column

position Pin to Top / Bottom

The position prop of type: 'row-pinning' takes top or bottom, pinning rows to the header or footer. v-model:row-pinning exposes two id groups: top and bottom:

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

const data = makePeople(8)
const pinning = ref<RowPinningState>({ top: ['P0001'], bottom: ['P0004', 'P0008'] })

const columns: DataTableColumn<Person>[] = [
  { type: 'row-pinning', position: 'top', header: '' },
  { accessorKey: 'id', header: 'ID' },
  { accessorKey: 'name', header: '姓名' },
  { accessorKey: 'department', header: '部门' },
  { accessorKey: 'role', header: '岗位' }
]
</script>

<template>
  <MDataTable v-model:row-pinning="pinning" row-key="id" :columns="columns" :data="data" :ui="{ root: 'max-h-65' }" />
</template>

buttonProps Dynamic Pin Button

buttonProps receives the pinned state and returns icon, color and variant, distinguishing pinned from unpinned rows:

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

const data = makePeople(6)

const columns: DataTableColumn<Person>[] = [
  {
    type: 'row-pinning',
    buttonProps: ({ pinned }) => pinned
      ? { icon: 'i-lucide-pin-off', color: 'primary', variant: 'outline' }
      : { icon: 'i-lucide-pin', color: 'neutral', variant: 'ghost' }
  },
  { accessorKey: 'name', header: '姓名' },
  { accessorKey: 'department', header: '部门' },
  { accessorKey: 'role', header: '岗位' }
]
</script>

<template>
  <MDataTable row-key="id" :columns="columns" :data="data" />
</template>

Actions Column

maxInline Inline and Overflow

maxInline controls the number of inline buttons; extras collapse into an overflow menu. action.divider inserts a separator in the menu:

姓名部门操作
李勇
产品
王超
市场
陈娜
设计
刘丽
运营
杨军
研发
<script setup lang="ts">
import type { DataTableColumn } from '@movk/nuxt'
import type { Person } from '~/composables/useTableMock'

const data = makePeople(5)
const toast = useToast()
const notify = (msg: string): void => { toast.add({ title: msg, duration: 1500 }) }

const columns: DataTableColumn<Person>[] = [
  { accessorKey: 'name', header: '姓名' },
  { accessorKey: 'department', header: '部门' },
  {
    type: 'actions',
    minSize: 80,
    maxInline: 2,
    actions: [
      { key: 'view', buttonProps: { icon: 'i-lucide-eye', variant: 'ghost', size: 'xs' }, onClick: ({ row }) => notify(`查看 ${row.name}`) },
      { key: 'edit', buttonProps: { icon: 'i-lucide-pencil', variant: 'ghost', size: 'xs' }, onClick: ({ row }) => notify(`编辑 ${row.name}`) },
      { key: 'archive', buttonProps: { icon: 'i-lucide-archive', label: '归档', size: 'xs' }, onClick: ({ row }) => notify(`归档 ${row.name}`) },
      { key: 'delete', divider: true, buttonProps: { icon: 'i-lucide-trash-2', label: '删除', color: 'error', size: 'xs' }, onClick: ({ row }) => notify(`删除 ${row.name}`) }
    ]
  }
]
</script>

<template>
  <MDataTable row-key="id" :columns="columns" :data="data" />
</template>

actions Array and Functional

actions accepts either a static array or a (ctx) => DataTableAction[] function to derive actions per row:

姓名部门操作
李勇
产品
王超
市场
陈娜
设计
刘丽
运营
杨军
研发
<script setup lang="ts">
import type { DataTableAction, DataTableActionsColumn, DataTableColumn } from '@movk/nuxt'
import type { Person } from '~/composables/useTableMock'

const data = makePeople(5)
const toast = useToast()
const notify = (msg: string): void => { toast.add({ title: msg, duration: 1500 }) }

const base: DataTableAction<Person>[] = [
  { key: 'view', buttonProps: { icon: 'i-lucide-eye', variant: 'ghost', size: 'xs' }, onClick: ({ row }) => notify(`查看 ${row.name}`) },
  { key: 'edit', buttonProps: { icon: 'i-lucide-pencil', variant: 'ghost', size: 'xs' }, onClick: ({ row }) => notify(`编辑 ${row.name}`) }
]

const actions: DataTableActionsColumn<Person>['actions'] = ({ row: { original } }) => {
  const extras: DataTableAction<Person>[] = []
  if (original.department === '研发')
    extras.push({ key: 'resetPwd', buttonProps: { icon: 'i-lucide-key-round', variant: 'ghost', size: 'xs' }, onClick: ({ row }) => notify(`重置密码 ${row.name}`) })
  if (original.department === '设计')
    extras.push({ key: 'transfer', buttonProps: { icon: 'i-lucide-share-2', variant: 'ghost', size: 'xs' }, onClick: ({ row }) => notify(`转交 ${row.name}`) })
  return [...base, ...extras]
}

const columns: DataTableColumn<Person>[] = [
  { accessorKey: 'name', header: '姓名' },
  { accessorKey: 'department', header: '部门' },
  { type: 'actions', maxInline: 4, actions, size: 120 }
]
</script>

<template>
  <MDataTable row-key="id" :columns="columns" :data="data" />
</template>

Async Action Auto Loading

When onClick returns a Promise, the button automatically enters a loading state and is disabled to prevent re-triggering (inline buttons only):

姓名部门操作
李勇
产品
王超
市场
陈娜
设计
刘丽
运营
杨军
研发
<script setup lang="ts">
import type { DataTableColumn } from '@movk/nuxt'
import type { Person } from '~/composables/useTableMock'

const data = makePeople(5)
const toast = useToast()
const notify = (msg: string): void => { toast.add({ title: msg, duration: 1500 }) }

const columns: DataTableColumn<Person>[] = [
  { accessorKey: 'name', header: '姓名' },
  { accessorKey: 'department', header: '部门' },
  {
    type: 'actions',
    maxInline: 2,
    size: 120,
    actions: [
      { key: 'view', buttonProps: { icon: 'i-lucide-eye', variant: 'ghost', size: 'xs' }, onClick: ({ row }) => notify(`查看 ${row.name}`) },
      {
        key: 'sync',
        buttonProps: { icon: 'i-lucide-refresh-cw', label: '同步', size: 'xs' },
        onClick: async ({ row }) => {
          await new Promise(r => setTimeout(r, 1200))
          notify(`同步完成 ${row.name}`)
        }
      }
    ]
  }
]
</script>

<template>
  <MDataTable row-key="id" :columns="columns" :data="data" />
</template>

confirm Confirmation Dialog

confirm: true pops up a MessageBox before executing; confirmProps can dynamically return title, type, description and confirmText:

姓名状态操作
李勇
leave
王超
offboarded
陈娜
active
刘丽
leave
杨军
offboarded
<script setup lang="ts">
import type { DataTableColumn } from '@movk/nuxt'
import type { Person } from '~/composables/useTableMock'

const data = makePeople(5)
const toast = useToast()
const notify = (msg: string): void => { toast.add({ title: msg, duration: 1500 }) }

const columns: DataTableColumn<Person>[] = [
  { accessorKey: 'name', header: '姓名' },
  { accessorKey: 'status', header: '状态' },
  {
    type: 'actions',
    size: 120,
    actions: [
      { key: 'edit', buttonProps: { icon: 'i-lucide-pencil', variant: 'ghost', size: 'xs' }, onClick: ({ row }) => notify(`编辑 ${row.name}`) },
      {
        key: 'delete',
        buttonProps: { icon: 'i-lucide-trash-2', label: '删除', color: 'error', size: 'xs' },
        confirm: true,
        confirmProps: ({ row }) => ({
          type: row.status === 'active' ? 'warning' : 'error',
          title: `删除 ${row.name}`,
          description: row.status === 'active' ? '该员工在职,删除前请二次确认。' : '已离职员工删除后无法恢复。',
          confirmText: '我已确认'
        }),
        onClick: ({ row }) => notify(`已删除 ${row.name}`)
      }
    ]
  }
]
</script>

<template>
  <MDataTable row-key="id" :columns="columns" :data="data" />
</template>

Dynamic disabled and visibility

action.disabled and action.visibility accept a boolean or (ctx) => boolean to derive disabled or hidden state per row:

姓名状态操作
李勇
leave
王超
offboarded
陈娜
active
刘丽
leave
杨军
offboarded
黄涛
active
赵伟
leave
吴敏
offboarded
<script setup lang="ts">
import type { DataTableColumn } from '@movk/nuxt'
import type { Person } from '~/composables/useTableMock'

const data = makePeople(8)
const toast = useToast()
const notify = (msg: string): void => { toast.add({ title: msg, duration: 1500 }) }

const columns: DataTableColumn<Person>[] = [
  { accessorKey: 'name', header: '姓名' },
  { accessorKey: 'status', header: '状态' },
  {
    type: 'actions',
    maxInline: 3,
    size: 150,
    actions: [
      { key: 'view', buttonProps: { icon: 'i-lucide-eye', variant: 'ghost', size: 'xs' }, onClick: ({ row }) => notify(`查看 ${row.name}`) },
      { key: 'edit', buttonProps: { icon: 'i-lucide-pencil', variant: 'ghost', size: 'xs' }, disabled: ({ row }) => row.status !== 'active', onClick: ({ row }) => notify(`编辑 ${row.name}`) },
      { key: 'recall', buttonProps: { icon: 'i-lucide-undo-2', label: '召回', size: 'xs' }, visibility: ({ row }) => row.status === 'offboarded', onClick: ({ row }) => notify(`召回 ${row.name}`) }
    ]
  }
]
</script>

<template>
  <MDataTable row-key="id" :columns="columns" :data="data" />
</template>

Shared Fields for Special Columns

selection, index, row-pinning and actions columns support the same visibility, pinable, resizable, size, minSize, maxSize, align and fixed as data columns. Special columns default to a fixed width of 60; providing minSize / maxSize (without size) switches them to auto-sizing — minSize is the lower bound and maxSize is the drag upper limit. The sole right-side actions column (fixed: 'right') can set minSize to expand with the number of buttons. See Data Column Width Rules for details.

姓名
部门
操作
李勇
产品
王超
市场
陈娜
设计
刘丽
运营
杨军
研发
<script setup lang="ts">
import type { DataTableAction, DataTableColumn } from '@movk/nuxt'
import type { Person } from '~/composables/useTableMock'

const data = makePeople(5)
const toast = useToast()
const actions: DataTableAction<Person>[] = [
  { key: 'view', buttonProps: { icon: 'i-lucide-eye', variant: 'ghost', size: 'xs' }, onClick: ({ row }) => { toast.add({ title: `查看 ${row.name}`, duration: 1500 }) } }
]

const columns: DataTableColumn<Person>[] = [
  { type: 'selection', pinable: true, resizable: true },
  { type: 'index', visibility: false },
  { type: 'row-pinning', pinable: false },
  { accessorKey: 'name', header: '姓名' },
  { accessorKey: 'department', header: '部门', resizable: true },
  { type: 'actions', size: 80, actions }
]
</script>

<template>
  <MDataTable row-key="id" :columns="columns" :data="data" />
</template>
Copyright © 2025 - 2026 YiXuan - MIT License