Introduction
Overview
MDataTable is a declarative data table built on @tanstack/vue-table and Nuxt UI UTable. It consolidates column definitions, sorting, column pinning, column resize, selection, expansion, tree data, pagination and load more into a single columns array and a handful of props, while providing complete TypeScript type inference.
- Declarative columns — Describe columns with an array of objects; data columns, selection columns, index columns, expand columns, actions columns and group columns are distinguished by
accessorKeyortype. - Global + per-column toggles —
sortable,pinable,resizable,truncate,tooltipand others can be enabled globally and overridden per column, or driven by a function for dynamic behavior. - Type-safe — Columns, cell callbacks and event handlers are all driven by types such as
DataTableColumn<T>andDataTableProps<T>.
Usage
A single columns array is enough to declare selection columns, index columns, fixed columns, cell rendering and actions columns. Combine with row-key for row identification, sortable/pinable/resizable for sorting, column pinning and column resize, and v-model:pagination for pagination — no manual assembly required:
姓名 | # | 部门 | 岗位 | 状态 | 薪资 | 操作 | |
|---|---|---|---|---|---|---|---|
李勇 | 1 | 产品 | UI 设计师 | 休假 | ¥8,257 | ||
王超 | 2 | 市场 | 运营专员 | 已离职 | ¥8,514 | ||
陈娜 | 3 | 设计 | 全栈工程师 | 在职 | ¥8,771 | ||
刘丽 | 4 | 运营 | 数据分析师 | 休假 | ¥9,028 | ||
杨军 | 5 | 研发 | 后端工程师 | 已离职 | ¥9,285 | ||
黄涛 | 6 | 产品 | 产品经理 | 在职 | ¥9,542 |
<script setup lang="ts">
import type { DataTableColumn, DataTableDataColumn, PaginationState } from '@movk/nuxt'
import type { Person } from '~/composables/useTableMock'
import { UBadge } from '#components'
const data = makePeople(40)
const pagination = ref<PaginationState>({ pageIndex: 0, pageSize: 6 })
const toast = useToast()
const notify = (msg: string): void => { toast.add({ title: msg, duration: 1500 }) }
const STATUS_LABEL: Record<Person['status'], string> = {
active: '在职',
leave: '休假',
offboarded: '已离职'
}
const STATUS_COLOR: Record<Person['status'], 'success' | 'warning' | 'neutral'> = {
active: 'success',
leave: 'warning',
offboarded: 'neutral'
}
const statusCell: DataTableDataColumn<Person>['cell'] = ({ getValue }) => {
const v = getValue<Person['status']>()
return h(UBadge, { color: STATUS_COLOR[v], variant: 'subtle' }, () => STATUS_LABEL[v])
}
const moneyCell: DataTableDataColumn<Person>['cell'] = ({ getValue }) => `¥${getValue<number>().toLocaleString()}`
const columns: DataTableColumn<Person>[] = [
{ type: 'selection' },
{ type: 'index' },
{ accessorKey: 'name', header: '姓名', size: 120, fixed: 'left', pinable: true },
{ accessorKey: 'department', header: '部门', size: 100 },
{ accessorKey: 'role', header: '岗位', size: 160, resizable: true },
{ accessorKey: 'status', header: '状态', size: 100, cell: statusCell },
{ accessorKey: 'salary', header: '薪资', size: 120, align: 'right', cell: moneyCell },
{
type: 'actions',
size: 100,
maxInline: 2,
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', variant: 'ghost', color: 'error', size: 'xs' }, onClick: ({ row }) => notify(`删除 ${row.name}`) }
]
}
]
</script>
<template>
<MDataTable
v-model:pagination="pagination"
row-key="id"
:columns="columns"
:data="data"
sortable
bordered
/>
</template>
Sample Data
All examples in this chapter derive their data from the following utility function. You can copy it directly into your project:
export interface Person {
id: string
name: string
email: string
department: 'R&D' | 'Design' | 'Product' | 'Operations' | 'Marketing'
role: string
level?: 'P5' | 'P6' | 'P7' | 'P8'
status: 'active' | 'leave' | 'offboarded'
salary: number
joinedAt: string
address: string
bio: string
children?: Person[]
}
const FIRST = ['Zhang', 'Li', 'Wang', 'Chen', 'Liu', 'Yang', 'Huang', 'Zhao', 'Wu', 'Zhou', 'Xu', 'Sun']
const LAST = ['Wei', 'Fang', 'Na', 'Min', 'Jing', 'Li', 'Qiang', 'Lei', 'Jun', 'Yang', 'Yong', 'Tao', 'Ming', 'Chao']
const DEPARTMENTS: Person['department'][] = ['R&D', 'Design', 'Product', 'Operations', 'Marketing']
const ROLES = ['Frontend Engineer', 'Backend Engineer', 'Full Stack Engineer', 'UI Designer', 'Product Manager', 'Data Analyst', 'Operations Specialist']
const LEVELS: Person['level'][] = ['P5', 'P6', 'P7', 'P8']
const STATUS: Person['status'][] = ['active', 'leave', 'offboarded']
const ADDRESSES = [
'708, IBM Tower, 2 Boyun Rd, Zhangjiang Hi-Tech Park, Pudong, Shanghai',
'1502, Zhongguancun Tower A, 27 Zhongguancun St, Haidian, Beijing',
'2301, Block B, Bay Technology Ecology Park, 9 Gaoxin Nan 9th Rd, Nanshan, Shenzhen',
'38F, R&F Yingkai Plaza, 16 Huaxia Rd, Pearl River New Town, Tianhe, Guangzhou',
'318, Building 6, Alibaba Xixi Campus, 969 Wenyi W Rd, Yuhang, Hangzhou',
'1207, Building 5, Block D, Tianfu Software Park, 1199 Tianfu Ave, Hi-Tech Zone, Chengdu',
'19F, Gaoke Rongyu Tower, 222 Jiangdong Zhong Rd, Jianye, Nanjing',
'1808, Block B, Phase 3, Optics Valley International Enterprise Center, 70 Optics Valley Ave, Hongshan, Wuhan'
]
const BIO_TEMPLATES = [
'Led supply chain and fulfillment infrastructure for a cross-border e-commerce team; spearheaded a real-time scheduling system overhaul handling millions of daily orders, with a focus on stability and cost efficiency.',
'Long-term investment in big data platforms and offline computation scheduling; built a PB-scale data warehouse and governance framework with strong experience in data quality and access control.',
'Focused on design systems and component libraries, driving visual consistency across products and leading multiple Design Token and dark-mode rollouts.',
'Responsible for an in-house low-code platform and BFF middleware layer; delivered internal scaffolding and CLI tooling with an emphasis on developer productivity and maintainability.',
'Deep expertise in real-time audio/video and WebRTC interactive scenarios; led latency optimization and weak-network degradation strategies for live rooms with tens of millions of concurrent users.',
'Owned permissions, billing and multi-tenancy for a B2B SaaS product with full end-to-end experience in RBAC, ABAC and compliance auditing.',
'Engineered risk control and anti-abuse algorithms; built real-time feature platforms and rules engines covering payment, marketing and social scenarios.',
'Focused on mobile performance optimization and cross-platform rendering frameworks; led end-to-end governance of startup time, memory usage and bundle size.'
]
function pick<T>(list: readonly T[], seed: number): T {
return list[seed % list.length] as T
}
export function makePerson(seed: number): Person {
const first = pick(FIRST, seed)
const last = pick(LAST, seed * 31 + 7)
const dept = pick(DEPARTMENTS, seed * 7)
return {
id: `P${String(seed).padStart(4, '0')}`,
name: `${first} ${last}`,
email: `user${seed}@movk.dev`,
department: dept,
role: pick(ROLES, seed * 3),
level: pick(LEVELS, seed * 11),
status: pick(STATUS, seed * 13),
salary: 8000 + (seed * 257) % 50000,
joinedAt: new Date(2018 + (seed % 7), seed % 12, 1 + (seed % 27)).toISOString().slice(0, 10),
address: pick(ADDRESSES, seed * 17),
bio: pick(BIO_TEMPLATES, seed * 19)
}
}
export function makePeople(count: number, offset = 0): Person[] {
return Array.from({ length: count }, (_, i) => makePerson(offset + i + 1))
}
export function makePeopleTree(rootCount = 5, childPerRoot = 3, depth = 1): Person[] {
const build = (seed: number, level: number): Person => {
const person = makePerson(seed)
if (level >= depth) return person
return {
...person,
role: 'Team Lead',
level: undefined,
children: Array.from({ length: childPerRoot }, (_, j) =>
build(seed * 100 + j + 1, level + 1))
}
}
return Array.from({ length: rootCount }, (_, i) => build(i + 1, 0))
}
API
Props
| Prop | Default | Type |
|---|---|---|
as | 'div' | anyThe element or component this component should render as. |
rowKey | string & {} | keyof T & string行唯一标识字段,自动派生 getRowId;与 getRowId 同传时后者优先 | |
columns | DataTableColumn<T>[] | |
bordered | false | boolean | { color?: string; width?: string; style?: "solid" | "dashed" | "dotted" | "double"; }纵向边框,传对象可定制 color/width/style |
emptyCell | '-' | string | false | (props: CellContext<T, unknown>): any空值占位符 |
pinable | false | boolean | ((col: DataTableDataColumn<T>) => boolean)启用列固定按钮,传函数可按列动态决定 |
pinButtonProps | ButtonProps | (ctx: DataTablePinButtonContext<T>): ButtonProps | |
sortable | false | boolean | ((col: DataTableDataColumn<T>) => boolean)启用列排序,传函数可按列动态决定 |
sortButtonProps | ButtonProps | (ctx: DataTableSortButtonContext<T>): ButtonProps | |
actionButtonProps | ButtonProps | (ctx: DataTableActionButtonContext<T>): ButtonProps全局 action 按钮 props,与列级 action.buttonProps 深度合并,列级优先 | |
actionsMaxInline | 3 | number行内最多展示多少 action 按钮,超出折叠到 overflow |
actionsOverflowTrigger | ButtonProps | (ctx: CellContext<T, unknown>): ButtonProps | |
resizable | false | boolean | ((col: DataTableDataColumn<T>) => boolean)启用列宽拖拽,传函数可按列动态决定 |
columnResizeMode | 'onChange' | "onChange" | "onEnd"
|
density | "compact" | "normal" | "comfortable" | { th?: string | ((cell: Header<T, unknown>) => string); td?: string | ((cell: Cell<T, unknown>) => string); }单元格内边距密度 | |
meta | TableMeta<T> | |
rowClass | string | (row: T): string | |
rowStyle | string | Record<string, string> | (row: T): string | Record<string, string> | |
tooltip | false | number | false | true | (ctx: CellContext<T, unknown>): number | boolean单元格溢出 Tooltip:true 单行 / number 多行 / false 禁用 / 函数 动态 |
tooltipProps | OmitByKey<TooltipProps, "text"> | |
truncate | true | number | false | true | (ctx: CellContext<T, unknown>): number | boolean单元格文本截断:true 单行 / number 多行 / false 禁用 / 函数 动态 |
sortingOptions | Omit<SortingOptions<T>, "getSortedRowModel" | "onSortingChange"> | |
columnSizingOptions | Omit<ColumnSizingOptions, "onColumnSizingChange" | "onColumnSizingInfoChange"> | |
columnPinningOptions | Omit<ColumnPinningOptions, "onColumnPinningChange"> | |
rowSelectionOptions | Omit<RowSelectionOptions<T>, "onRowSelectionChange"> | |
childrenKey | string & {} | keyof T & string子行字段名,设置后启用树形模式 | |
indentSize | '1rem' | string | number | (ctx: CellContext<T, unknown>): string树形缩进:number 每层缩进 px / string CSS 值 / 函数 动态返回 CSS |
defaultExpanded | false | number | false | true | (row: T, depth: number): boolean树形模式下的默认展开行为,仅在未提供 expanded / expandedKeys 时生效。true 展开全部父级行,number 展开 depth 小于该值的父级行,函数按行与深度自定义 |
expandedOptions | Omit<ExpandedOptions<T>, "getExpandedRowModel" | "onExpandedChange"> | |
columnVisibilityKeys | string[]可见列白名单(数组形) | |
columnVisibilityExcludeKeys | string[]隐藏列黑名单(数组形),与 columnVisibilityKeys 互斥,同传时白名单优先 | |
rowSelectionKeys | string[]选中行 id 列表(数组形) | |
expandedKeys | string[]展开行 id 列表(数组形) | |
onSelect | DataTableSelectHandler<T> | |
onHover | DataTableHoverHandler<T> | |
onRowContextmenu | (e: Event, row: TableRow<T>): void | ((e: Event, row: TableRow<T>) => void)[] | |
paginationOptions | Omit<PaginationOptions, "onPaginationChange">分页配置,透传给 TanStack / UTable
| |
sticky | true | boolean | "header" | "footer"粘性表头 |
paginationUi | DataTablePaginationUi & { ui?: { root?: SlotClass; summary?: SlotClass; summaryText?: SlotClass; selectedCount?: SlotClass; actions?: SlotClass; pageSizeSelect?: SlotClass; pagination?: SlotClass; }; } | |
loadMore | () => void | Promise<void>触底加载回调,传入即启用无限滚动模式(自动隐藏内置分页、async 期间派生 loading) | |
loadMoreDistance | 100 | number触发 loadMore 的距底像素阈值 |
data | T[] | |
caption | string | |
virtualize | false | boolean | (Partial<Omit<VirtualizerOptions<Element, Element>, "count" | "getScrollElement" | "estimateSize" | "overscan">> & { overscan?: number; estimateSize?: number | ((index: number) => number); })Enable virtualization for large datasets. Note: row pinning is not supported when virtualization is enabled. |
empty | t('table.noData') | stringThe text to display when the table is empty. |
loadingColor | 'primary' | "primary" | "secondary" | "info" | "success" | "warning" | "error" | "important" | "neutral" |
loadingAnimation | 'carousel' | "carousel" | "carousel-inverse" | "swing" | "elastic" |
watchOptions | { deep: true } | WatchOptions<boolean>Use the |
globalFilterOptions | Omit<GlobalFilterOptions<T>, "onGlobalFilterChange"> | |
columnFiltersOptions | Omit<ColumnFiltersOptions<T>, "getFilteredRowModel" | "onColumnFiltersChange"> | |
visibilityOptions | Omit<VisibilityOptions, "onColumnVisibilityChange"> | |
groupingOptions | Omit<GroupingOptions, "onGroupingChange"> | |
rowPinningOptions | Omit<RowPinningOptions<T>, "onRowPinningChange"> | |
facetedOptions | FacetedOptions<T> | |
state | Partial<TableState> | |
onStateChange | (updater: Updater<TableState>) => void | |
renderFallbackValue | any | |
_features | TableFeature<any>[]An array of extra features that you can add to the table instance. | |
defaultColumn | Partial<ColumnDefBase<T, unknown> & StringHeaderIdentifier> | Partial<ColumnDefBase<T, unknown> & IdIdentifier<T, unknown>> | Partial<GroupColumnDefBase<T, unknown> & StringHeaderIdentifier> | Partial<GroupColumnDefBase<T, unknown> & IdIdentifier<T, unknown>> | Partial<AccessorKeyColumnDefBase<T, unknown> & Partial<StringHeaderIdentifier>> | Partial<AccessorKeyColumnDefBase<T, unknown> & Partial<IdIdentifier<T, unknown>>> | Partial<AccessorFnColumnDefBase<T, unknown> & StringHeaderIdentifier> | Partial<AccessorFnColumnDefBase<T, unknown> & IdIdentifier<T, unknown>>Default column options to use for all column defs supplied to the table. | |
getRowId | (originalRow: T, index: number, parent?: Row<T>) => stringThis optional function is used to derive a unique ID for any given row. If not provided the rows index is used (nested rows join together with | |
getSubRows | (originalRow: T, index: number) => T[]This optional function is used to access the sub rows for any given row. If you are using nested rows, you will need to use this function to return the sub rows object (or undefined) from the row. | |
initialState | InitialTableStateUse this option to optionally pass initial state to the table. This state will be used when resetting various table states either automatically by the table (eg. Table state will not be reset when this object changes, which also means that the initial state object does not need to be stable. | |
mergeOptions | (defaultOptions: TableOptions<T>, options: Partial<TableOptions<T>>) => TableOptions<T>This option is used to optionally implement the merging of table options. | |
cellpadding | string | number | |
cellspacing | string | number | |
summary | string | |
width | string | number | |
pagination | PaginationState | |
columnVisibility | VisibilityState | |
columnPinning | ColumnPinningState | |
columnSizing | ColumnSizingState | |
rowSelection | RowSelectionState | |
rowPinning | { top: [], bottom: [] } | RowPinningState |
sorting | [] | SortingState |
expanded | true | Record<string, boolean> | |
loading | boolean | |
stripe | false | boolean斑马纹 |
fitContent | false | boolean表格宽度由列宽内容决定(w-fit) |
expandOnRowClick | false | boolean |
selectOnRowClick | false | boolean |
canLoadMore | true | boolean是否还能加载更多 |
autoResetAll | booleanSet this option to override any of the | |
debugAll | booleanSet this option to | |
debugCells | booleanSet this option to | |
debugColumns | booleanSet this option to | |
debugHeaders | booleanSet this option to | |
debugRows | booleanSet this option to | |
debugTable | booleanSet this option to | |
ui | Record<string, ClassNameValue> & { wrapper?: SlotClass; base?: SlotClass; tbody?: SlotClass; th?: SlotClass; td?: SlotClass; root?: SlotClass; caption?: SlotClass; thead?: SlotClass; tfoot?: SlotClass; tr?: SlotClass; separator?: SlotClass; empty?: SlotClass; loading?: SlotClass; } |
Emits
| Event | Type |
|---|---|
update:pagination | [value: PaginationState] |
update:columnVisibility | [value: VisibilityState] |
update:columnPinning | [value: ColumnPinningState] |
update:columnSizing | [value: ColumnSizingState] |
update:rowSelection | [value: RowSelectionState] |
update:rowPinning | [value: RowPinningState] |
update:sorting | [value: SortingState] |
update:expanded | [value: ExpandedState] |
update:columnVisibilityKeys | [value: string[]] |
update:columnVisibilityExcludeKeys | [value: string[]] |
update:rowSelectionKeys | [value: string[]] |
update:expandedKeys | [value: string[]] |
Slots
| Slot | Type |
|---|---|
expanded | { row: Row<T>; } |
empty | any |
loading | any |
caption | any |
body-top | any |
body-bottom | any |
pagination | DataTablePaginationSlotProps<T> |
pagination-summary | { summaryText: string; selectedText: string; selectedCount: number; rowCount: number; rowCountKnown: boolean; from: number; to: number; page: number; pageCount: number; showSelectedCount: boolean; } |
pagination-actions | { tableApi: Table<T>; page: number; pageCount: number; pageSize: number; rowCount: number; pageSizes: number[]; pageSizeOptions: { label: string; value: number; }[]; showPageSizeSelect: boolean; setPage: (page: number) => void; setPageSize: (pageSize: unknown) => void; } |
Expose
Access the typed component instance (DataTableExposed<T>) via useTemplateRef.
| Name | Type |
|---|---|
tableRef | HTMLTableElement | null Native table element |
tableApi | Table<T> | null TanStack Table instance |
el | HTMLElement | null UTable root element (scroll container) |
scrollToTop(options?) | void Scroll to top |
clearSelection() | void Clear row selection |