Special Columns
Overview
Data columns bind fields via accessorKey; special columns are distinguished by type and do not need an accessorKey:
type | Purpose |
|---|---|
selection | Row selection (single/multiple, tree cascade strategies) |
index | Row index |
expand | Tree expansion (see Tree Data) |
row-pinning | Pin rows to top or bottom |
actions | Row action buttons |
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):
| 姓名 | 部门 | ||
|---|---|---|---|
李勇 | 产品 | ||
王超 | 市场 | ||
陈娜 | 设计 |
| 姓名 | 部门 | ||
|---|---|---|---|
李勇 | 产品 | ||
王超 | 市场 | ||
陈娜 | 设计 |
| 姓名 | 部门 | ||
|---|---|---|---|
李勇 | 产品 | ||
王超 | 市场 | ||
陈娜 | 设计 |
<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>
Columns
Data column configuration — fixed columns, sorting, column pinning, column resize, text truncation, tooltips, visibility and functional per-column configuration.
Rows & Interaction
Row selection, expandable detail slot, click behaviors, dynamic row class/style, hover, context menu and imperative control.