PrimeVue TreeTable 组件完全指南:分层数据表格的导入、交互、懒加载与主题定制
【免费下载链接】primevueNext Generation Vue UI Component Library项目地址: https://gitcode.com/GitHub_Trending/pr/primevue
TreeTable 是 PrimeVue 中用于以表格形式展示层级(树形)数据的核心组件,本指南以 treetable.md 为骨架,结合 TreeTable.vue 源码深入讲解其数据模型、受控展开、列控制、右键菜单、筛选、懒加载、无障碍与主题体系。读完本文,你将能够独立搭建一个具备分页、筛选、懒加载与自定义模板的树形表格,并掌握其底层实现原理。
数据模型:TreeNode 与列声明
TreeTable 使用一个value属性接收TreeNode[]数组,每个节点通过children字段递归嵌套形成层级。TreeNode的完整接口定义在 TreeNode.d.ts,核心字段包括:
| 字段 | 类型 | 说明 |
|---|---|---|
key | string | 节点唯一键,必填,受控展开/选中都以它为准 |
label | string | 节点标签 |
data | any | 节点承载的业务数据(TreeTable 的列字段一般从data中取值) |
children | TreeNode[] | 子节点数组 |
leaf | boolean | 是否叶子节点,懒加载时用于判断是否还需要展开回调 |
selectable | boolean | 是否可选中 |
icon/expandedIcon/collapsedIcon | string | 节点图标 |
本仓库 NodeService.js 中的getTreeTableNodesData()给出了典型结构——每个节点含key与data,data内是name、size、type字段,这正是示例中三个 Column 的数据来源:
{ key: '0', data: { name: 'Applications', size: '100kb', type: 'Folder' }, children: [ { key: '0-0', data: { name: 'Vue', size: '25kb', type: 'Folder' }, children: [...] } ] }列结构则完全复用 DataTable 的Column组件体系(源码见 TreeTable.vue 中通过HelperSet收集Column子组件并渲染HeaderCell/BodyCell),因此你可以把 TreeTable 理解为“TreeNode 数据 + Column 列配置 + 树节点交互”的组合。
基本用法与节点展开
导入组件:
import TreeTable from 'primevue/treetable'; import Column from 'primevue/column';value传入 TreeNode 数组,Column作为子组件声明列。负责切换节点展开/折叠的列必须开启expander,展开箭头才会渲染在该列上:
<TreeTable :value="nodes" tableStyle="min-width: 50rem"> <Column field="name" header="Name" expander style="width: 34%"></Column> <Column field="size" header="Size" style="width: 33%"></Column> <Column field="type" header="Type" style="width: 33%"></Column> </TreeTable>tableStyle/tableClass用于控制<table>元素的内联样式与类名。在源码 TreeTable.vue 中,onNodeToggle维护d_expandedKeys并依次触发node-expand/node-collapse与update:expandedKeys,这就是展开状态的完整流转链路。
受控展开:expandedKeys
默认状态下展开状态由组件内部维护;当你需要程序化控制时(例如“全部展开/全部折叠”按钮),使用v-model:expandedKeys进入受控模式。expandedKeys是一个以节点key为属性名、布尔值为属性值的对象,如{ '0-0': true }:
<Button @click="toggleApplications" label="Toggle Applications" /> <TreeTable v-model:expandedKeys="expandedKeys" :value="nodes" class="mt-6" tableStyle="min-width: 50rem"> <Column field="name" header="Name" expander style="width: 34%"></Column> <Column field="size" header="Size" style="width: 33%"></Column> <Column field="type" header="Type" style="width: 33%"></Column> </TreeTable>const nodes = ref(); const expandedKeys = ref({}); const toggleApplications = () => { let _expandedKeys = { ...expandedKeys.value }; if (_expandedKeys['0']) delete _expandedKeys['0']; else _expandedKeys['0'] = true; expandedKeys.value = _expandedKeys; }注意切换时先拷贝再修改、最后整体替换对象,以触发响应式更新;删除键即折叠,设置true即展开。源码的watch.expandedKeys会将外部新值同步到内部状态d_expandedKeys(TreeTable.vue),所以受控与非受控可以随时切换。
动态列与列可见性控制
动态列
列定义本身是响应式数据,用v-for即可程序化生成列,expander也可以作为列配置的一部分:
<TreeTable :value="nodes" tableStyle="min-width: 50rem"> <Column v-for="col of columns" :key="col.field" :field="col.field" :header="col.header" :expander="col.expander"></Column> </TreeTable>const columns = ref([ { field: 'name', header: 'Name', expander: true }, { field: 'size', header: 'Size' }, { field: 'type', header: 'Type' } ]);用 MultiSelect 控制列可见性
将列配置与MultiSelect联动,即可实现列显隐:
<TreeTable :value="nodes" tableStyle="min-width: 50rem"> <template #header> <div style="text-align:left"> <MultiSelect :modelValue="selectedColumns" @update:modelValue="onToggle" :options="columns" optionLabel="header" class="w-full sm:w-64" display="chip"/> </div> </template> <Column field="name" header="Name" :expander="true"></Column> <Column v-for="col of selectedColumns" :field="col.field" :header="col.header" :key="col.field"></Column> </TreeTable>const columns = ref([ {field: 'size', header: 'Size'}, {field: 'type', header: 'Type'} ]); const selectedColumns = ref(columns.value); const onToggle = (val) => { selectedColumns.value = columns.value.filter(col => val.includes(col)); };核心思路是onToggle按 MultiSelect 返回的选中项过滤出可见列,v-for渲染的列集合随之变化,实现按条件控制列可见性。
右键菜单集成:ContextMenu
TreeTable 对ContextMenu有专门集成:开启contextMenu属性后,右键行会触发row-contextmenu事件,通过v-model:contextMenuSelection双向绑定当前右键选中的节点,配合菜单的show(event.originalEvent)弹出菜单:
<ContextMenu ref="cm" :model="menuModel" @hide="selectedNode = null" /> <TreeTable v-model:contextMenuSelection="selectedNode" :value="nodes" contextMenu @row-contextmenu="onRowContextMenu" tableStyle="min-width: 50rem"> <Column field="name" header="Name" expander style="width: 34%"></Column> <Column field="size" header="Size" style="width: 33%"></Column> <Column field="type" header="Type" style="width: 33%"></Column> </TreeTable>const cm = ref(); const selectedNode = ref(); const menuModel = ref([ { label: 'View', icon: 'pi pi-fw pi-search', command: () => this.viewNode(this.selectedNode) }, { label: 'Delete', icon: 'pi pi-fw pi-times', command: () => this.deleteNode(this.selectedNode) } ]); const onRowContextMenu = (event) => { cm.value.show(event.originalEvent); };菜单项command中读取selectedNode即可对右键节点执行操作(如删除后通过递归filterNodes从nodes中移除该节点子树)。底层事件对象TreeTableRowContextMenuEvent携带originalEvent与node(见 TreeTable.d.ts)。
筛选:lenient 与 strict 两种模式
TreeTable 的筛选由三部分组成:
- 列上设置
filter属性 +#filter插槽放置筛选控件; filters对象保存各字段与global的筛选值;filterMode决定筛选策略。
<SelectButton v-model="filterMode" optionLabel="label" dataKey="label" :options="filterOptions" /> <TreeTable :value="nodes" :filters="filters" :filterMode="filterMode.value"> <template #header> <div class="flex justify-end"> <IconField> <InputIcon class="pi pi-search" /> <InputText v-model="filters['global']" placeholder="Global Search" /> </IconField> </div> </template> <template #empty> No customers found.</template> <Column field="name" header="Name" expander style="min-width: 12rem"> <template #filter> <InputText v-model="filters['name']" type="text" placeholder="Filter by name" /> </template> </Column> <Column field="size" header="Size" style="min-width: 12rem"> <template #filter> <InputText v-model="filters['size']" type="text" placeholder="Filter by size" /> </template> </Column> <Column field="type" header="Type" style="min-width: 12rem"> <template #filter> <InputText v-model="filters['type']" type="text" placeholder="Filter by type" /> </template> </Column> </TreeTable>const nodes = ref(); const filters = ref({}); const filterMode = ref({ label: 'Lenient', value: 'lenient' }); const filterOptions = ref([ { label: 'Lenient', value: 'lenient' }, { label: 'Strict', value: 'strict' } ]);两种模式的区别(源码见 TreeTable.vue):
- lenient(宽松,默认):当查询命中某个节点时,不再深入搜索该节点的子节点,因为其所有后代都会被一并包含;
- strict(严格):当查询命中某个节点时,继续遍历其所有后代节点,过滤结果只保留真正命中的分支。
从实现看,filter()对每个节点分别做“局部筛选”(按列filterField/field匹配,默认filterMatchMode为startsWith)与“全局筛选”(filters['global'],使用contains约束),两者都通过FilterService.filters与findFilteredNodes/isFilterMatched结合strict标志决定分支是否保留。filterLocale可指定筛选时使用的 locale,默认取宿主环境的当前 locale。
分页与懒加载
内置分页
TreeTable 复用独立 Paginator 组件(源码中通过TTPaginator分别渲染在顶部、底部或两端,TreeTable.vue)。开启分页只需:
<TreeTable :value="nodes" :paginator="true" :rows="10" :totalRecords="nodes.length" tableStyle="min-width: 50rem"> ... </TreeTable>相关属性:rows(每页行数)、first(首行索引,默认 0)、totalRecords(总记录数,不传时默认取value长度)、paginatorPosition(top/bottom/both,默认bottom)、rowsPerPageOptions(每页条数下拉选项)、paginatorTemplate(默认FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink RowsPerPageDropdown,还支持JumpToPageDropdown、JumpToPageInput、CurrentPageReport)、currentPageReportTemplate(默认({currentPage} of {totalPages}),可用{currentPage}、{totalPages}、{rows}、{first}、{last}、{totalRecords}占位符)。
懒加载模式
大数据集下应开启lazy,配合totalRecords投影查询结果,让分页器按“逻辑总条数”渲染页码,而页面中只存在当前页的真实数据。此时只加载根节点,子节点通过nodeExpand回调按需加载:
<TreeTable :value="nodes" :lazy="true" :paginator="true" :rows="rows" :loading="loading" @nodeExpand="onExpand" @page="onPage" :totalRecords="totalRecords" tableStyle="min-width: 50rem"> <Column field="name" header="Name" :expander="true"></Column> <Column field="size" header="Size"></Column> <Column field="type" header="Type"></Column> </TreeTable>const nodes = ref(); const rows = ref(10); const loading = ref(false); const totalRecords = ref(0); onMounted(() => { loading.value = true; setTimeout(() => { loading.value = false; nodes.value = loadNodes(0, rows.value); totalRecords.value = 1000; // 投影查询得到的逻辑总数 }, 1000); }); const onExpand = (node) => { if (!node.children) { loading.value = true; setTimeout(() => { let lazyNode = {...node}; lazyNode.children = [ { data: { name: lazyNode.data.name + ' - 0', size: ..., type: 'File' } }, { data: { name: lazyNode.data.name + ' - 1', size: ..., type: 'File' } } ]; let newNodes = nodes.value.map(n => n.key === node.key ? lazyNode : n); loading.value = false; nodes.value = newNodes; }, 250); } }; const onPage = (event) => { loading.value = true; setTimeout(() => { loading.value = false; nodes.value = loadNodes(event.first, rows.value); // 用 event.first 作为偏移量请求后端 }, 1000); };关键点:
lazy模式下组件不本地排序/筛选/分页,page、sort、filter事件会携带first、rows、sortField、sortOrder、multiSortMeta、filters等完整上下文(事件类型见 TreeTable.d.ts),由你转发给后端;onExpand通过判断node.children是否存在来决定是否请求子节点,返回后替换原节点对象({...node, children})并整体更新nodes,以触发树重渲染;loading为true时显示加载遮罩(loadingMode为mask默认值)或加载图标(loadingMode="icon"),loadingIcon可自定义图标。
网格线、尺寸与模板
网格线与行尺寸
showGridlines在单元格之间显示网格线。行尺寸通过size属性切换:small、normal(默认)、large:
<TreeTable :value="nodes" :size="size.value" tableStyle="min-width: 50rem"> <Column field="name" header="Name" expander style="width: 34%"></Column> ... </TreeTable>const size = ref({ label: 'Normal', value: 'normal' }); const sizeOptions = ref([ { label: 'Small', value: 'small', class: 'sm' }, { label: 'Normal', value: 'normal' }, { label: 'Large', value: 'large', class: 'lg' } ]);表头/表尾与单元格模板
#header与#footer插槽支持任意自定义内容;Column的#body插槽可完全接管单元格渲染,且通过插槽作用域可拿到当前node数据(node.data、node.key等):
<TreeTable :value="nodes" tableStyle="min-width: 50rem"> <template #header> <div class="text-xl font-bold">File Viewer</div> </template> <Column field="name" header="Name" expander style="width: 250px"></Column> <Column field="size" header="Size" style="width: 150px"></Column> <Column field="type" header="Type" style="width: 150px"></Column> <Column style="width: 10rem"> <template #body> <div class="flex flex-wrap gap-2"> <Button type="button" icon="pi pi-search" rounded /> <Button type="button" icon="pi pi-pencil" rounded severity="success" /> </div> </template> </Column> <template #footer> <div class="flex justify-start"> <Button icon="pi pi-refresh" label="Reload" severity="warn" /> </div> </template> </TreeTable>事件、插槽与核心 API
事件(emits)
完整事件清单定义于 TreeTable.d.ts:
| 事件 | 说明 |
|---|---|
update:expandedKeys/update:selectionKeys/update:contextMenuSelection | 受控状态的双向绑定更新 |
update:first/update:rows/update:sortField/update:sortOrder/update:multiSortMeta | 分页与排序状态同步 |
page | 分页回调,携带first、rows等,懒加载时用于请求新页 |
sort | 排序回调,携带排序字段与顺序 |
filter | 筛选回调(非懒加载模式触发),携带filteredValue |
node-select/node-unselect | 节点选中/取消选中 |
node-expand/node-collapse | 节点展开/折叠(懒加载在node-expand中按需取数) |
row-contextmenu | 行右键菜单触发 |
column-resize-end | 列宽拖拽调整结束 |
插槽(slots)
header、footer、empty(空数据提示)、loadingicon、checkboxicon,以及分页器相关插槽paginatorcontainer、paginatorstart、paginatorend和各分页图标插槽(如paginatorfirstpagelinkicon)均可自定义(TreeTable.d.ts)。
无障碍与键盘操作
TreeTable 渲染为role="treegrid"结构(TreeTable.vue):表头/表体/表尾为rowgroup,行为row,表头单元格为columnheader,表体单元格为cell。可排序表头通过aria-sort暴露ascending/descending。行元素通过aria-expanded、aria-posinset、aria-setsize、aria-level描述层级(见 TreeTableRow.vue),选中时设置aria-selected。checkbox 选中模式下使用隐藏的原生 checkbox 元素;可编辑单元格因采用自定义模板,需要自行管理 ARIA 角色与属性。额外的 aria 属性可通过tableProps(如aria-label、aria-describedby)透传到<table>元素。
键盘支持:
可排序表头:
| 按键 | 功能 |
|---|---|
| tab | 在表头间移动焦点 |
| enter | 对该列排序 |
| space | 对该列排序 |
树节点导航:
| 按键 | 功能 |
|---|---|
| tab | 焦点进入组件时定位到第一个选中节点(无选中则定位首个元素);已在组件内则移动到下一个可聚焦元素 |
| shift + tab | 定位到最后一个选中节点(无选中则定位首个元素),或移动到上一个可聚焦元素 |
| enter / space | 选中焦点所在的树节点 |
| down arrow | 移动到下一个树节点 |
| up arrow | 移动到上一个树节点 |
| right arrow | 节点关闭则展开,否则移动到第一个子节点 |
| left arrow | 节点展开则折叠,否则移动到父节点 |
| home | 移动到同一层级的第一个节点 |
| end | 移动到同一层级的最后一个节点 |
Props 完整速查
以下为 treetable.md 与 TreeTable.d.ts 中的全部属性,按用途分组:
数据与标识:value(TreeNode[],必填)、dataKey(唯一标识字段,默认"key",可为函数)、totalRecords(逻辑总条数,默认取value.length)。
树状态:expandedKeys(受控展开键集合)、selectionKeys(选中键集合)、selectionMode(single/multiple/checkbox)、metaKeySelection(默认false,为true时多选需按 metaKey,触屏设备自动关闭)、contextMenu(启用右键集成)、contextMenuSelection(右键选中的节点)、indentation(子节点缩进因子,默认1,单位 rem)。
分页:paginator(默认false)、paginatorPosition(默认bottom)、alwaysShowPaginator(默认true)、paginatorTemplate、pageLinkSize(默认5)、rowsPerPageOptions、currentPageReportTemplate、rows、first(默认0)。
懒加载与加载态:lazy(默认false)、loading、loadingIcon、loadingMode(mask/icon,默认mask)。
排序:sortField、sortOrder、defaultSortOrder(默认1)、multiSortMeta、sortMode(single/multiple,默认single)、removableSort(列可回到未排序状态)。
筛选:filters(TreeTableFilterMeta,支持TreeTableFilterMetaData与操作符约束TreeTableOperatorFilterMetaData,见 TreeTable.d.ts)、filterMode(lenient/strict,默认lenient)、filterLocale。
外观与行为:rowHover(行悬停背景)、autoLayout(单元格宽度按内容伸缩)、resizableColumns(拖拽调整列宽)、columnResizeMode(fit/expand,默认fit)、showGridlines、scrollable、scrollHeight(固定像素或flex关键字)、size(small/large)、tableStyle、tableClass、tableProps。
主题与样式系统:dt(设计令牌生成作用域 CSS 变量)、pt(PassThrough 选项,透传属性到内部 DOM)、ptOptions(配置 pt 行为)、unstyled(移除核心内置样式)。
PassThrough 与主题定制
PassThrough 选项
pt允许将属性透传到组件内部每一个 DOM 节点,完整选项见 TreeTable.d.ts,主要包括:root、loading、mask、loadingIcon、header、pcPaginator(Paginator 组件)、tableContainer、table、thead、headerRow、tbody、row、emptyMessage、emptyMessageCell、tfoot、footerRow、footer、columnResizeIndicator、column(Column 辅助组件)与hooks(生命周期钩子)。每个选项既可以是属性对象,也可以是接收{ instance, props, state, context, attrs, parent, global }的上下文函数。
CSS 类名
主题样式基于以下根类名(对应p-treetable-*前缀):p-treetable(根)、p-treetable-loading、p-treetable-mask、p-treetable-loading-icon、p-treetable-header、p-treetable-paginator-[position]、p-treetable-table-container、p-treetable-table、p-treetable-thead、p-treetable-column-resizer、p-treetable-column-title、p-treetable-sort-icon、p-treetable-sort-badge、p-treetable-tbody、p-treetable-node-toggle-button、p-treetable-node-toggle-icon、p-treetable-node-checkbox、p-treetable-empty-message、p-treetable-tfoot、p-treetable-footer、p-treetable-column-resize-indicator。
设计令牌
TreeTable 通过设计令牌生成--p-treetable-*CSS 变量,可覆盖的关键令牌分组如下(完整清单见 treetable.md 中的 Design Tokens 表格):
| 令牌分组 | 主要 CSS 变量 |
|---|---|
| 根 | --p-treetable-transition-duration、--p-treetable-border-color |
| 表头 | --p-treetable-header-background、--p-treetable-header-color、--p-treetable-header-border-color、--p-treetable-header-padding |
| 表头单元格 | --p-treetable-header-cell-background、--p-treetable-header-cell-hover-background、--p-treetable-header-cell-selected-background、--p-treetable-header-cell-color、--p-treetable-header-cell-padding、--p-treetable-header-cell-focus-ring-width/style/color/offset/shadow |
| 行 | --p-treetable-row-background、--p-treetable-row-hover-background、--p-treetable-row-selected-background、--p-treetable-row-color、--p-treetable-row-focus-ring-* |
| 表体单元格 | --p-treetable-body-cell-border-color、--p-treetable-body-cell-padding、--p-treetable-body-cell-selected-border-color |
| 表尾 | --p-treetable-footer-cell-background、--p-treetable-footer-cell-border-color、--p-treetable-footer-cell-color、--p-treetable-footer-background、--p-treetable-footer-padding |
| 排序/加载 | --p-treetable-sort-icon-color、--p-treetable-sort-icon-hover-color、--p-treetable-sort-icon-size、--p-treetable-loading-icon-size |
| 节点开关 | --p-treetable-node-toggle-button-hover-background、--p-treetable-node-toggle-button-color、--p-treetable-node-toggle-button-size、--p-treetable-node-toggle-button-border-radius、--p-treetable-node-toggle-button-focus-ring-* |
| 列调整 | --p-treetable-column-resizer-width、--p-treetable-resize-indicator-width、--p-treetable-resize-indicator-color |
| 分页器 | --p-treetable-paginator-top-border-color/width、--p-treetable-paginator-bottom-border-color/width |
这些变量可直接在自定义主题或全局样式中覆盖,实现与设计系统一致的树形表格外观。
源码速览与进一步阅读
如果想深入理解实现,建议按以下路径阅读本仓库源码:
- TreeTable.vue:组件主体,包含模板结构、
onNodeToggle、filter()、排序、列宽调整与分页事件处理; - TreeTableRow.vue:单行渲染、层级缩进、键盘导航(
onKeyDown)与 ARIA 属性绑定; - HeaderCell.vue / BodyCell.vue / FooterCell.vue:表头(含排序与筛选)、表体、表尾单元格渲染;
- BaseTreeTable.vue:基于设计令牌的样式基座;
- TreeTable.d.ts:完整 Props、Slots、Emits、PassThrough 与状态/上下文类型定义;
- TreeNode.d.ts:树节点数据契约;
- NodeService.js:示例树数据(
getTreeTableNodesData()/getTreeTableNodes()); - 展示示例:treetable 文档页 与 treetable 页面(该目录下另有按功能拆分的
BasicDoc、FilterDoc、LazyLoadDoc等演示组件)。
结合这些文件,你就能把 TreeTable 的每个能力从“会用到”推进到“懂原理”,从而在真实项目中游刃有余地完成树形数据的展示、交互与性能优化。
【免费下载链接】primevueNext Generation Vue UI Component Library项目地址: https://gitcode.com/GitHub_Trending/pr/primevue
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考