在 Vue2 项目中实现树形表格功能时,开发者通常会面临多种技术选型。主流 UI 库如 Element UI 和 Ant Design Vue 都内置了树形表格组件,但当使用 Quasar 这类相对小众的组件库时,就需要寻找替代方案。本文将详细介绍两种实用的实现方案,并分享在实际项目中遇到的坑点与解决方案。
树形表格常用于展示具有层级关系的数据,比如:
这类数据的特点是父子节点之间存在明确的包含关系,需要可视化展示这种层级结构,同时保持表格的列对齐和交互功能。
首先安装 vue-table-with-tree-grid:
bash复制npm install vue-table-with-tree-grid --save
在 main.js 中全局注册:
javascript复制import Vue from 'vue'
import ZkTable from 'vue-table-with-tree-grid'
Vue.use(ZkTable)
javascript复制data() {
return {
config: {
stripe: false, // 是否显示斑马纹
border: true, // 是否显示边框
showHeader: true, // 是否显示表头
showSummary: false, // 是否显示合计行
showRowHover: true, // 鼠标悬停高亮
showIndex: false, // 是否显示索引列
treeType: true, // 开启树形结构
isFold: false, // 是否默认折叠
expandType: false, // 展开行类型
selectionType: false // 禁用自带选择框
},
treeColumns: [
{
prop: 'device',
label: 'Device',
align: 'left',
type: 'template',
template: 'device', // 使用自定义模板
minWidth: '150px'
}
// 其他列配置...
]
}
}
由于插件自带的选择功能存在问题,我们通过自定义模板实现单选:
html复制<zk-table :data="rows" :columns="treeColumns" :tree-type="config.treeType">
<!-- 自定义选择列 -->
<template slot="check" scope="scope">
<input
type="radio"
:name="radioGroupName"
:checked="isRowSelected(scope.row)"
@click.stop="handleRadioClick(scope.row)"
class="radio-select"
/>
</template>
<!-- 其他列模板... -->
</zk-table>
对应的处理逻辑:
javascript复制methods: {
isRowSelected(row) {
return this.selectedRow && this.selectedRow.id === row.id
},
handleRadioClick(row) {
this.selectedRow = this.isRowSelected(row) ? null : row
}
}
css复制/* 选中行样式 */
::v-deep .zk-table__body tr.selected-row > td {
background-color: #e3f2fd !important;
}
/* 鼠标悬停效果 */
::v-deep .zk-table__body tr:hover > td {
background-color: #f5f5f5 !important;
}
/* 单选按钮样式 */
.radio-select {
cursor: pointer;
width: 16px;
height: 16px;
}
注意:该插件存在事件参数与文档不符的问题,特别是 selection-change 事件无法正确获取行数据,这是采用自定义方案的主要原因。
相比 vue-table-with-tree-grid,VxeTable 具有以下优势:
创建 VxeTreeTable.vue 组件:
html复制<template>
<vxe-table
ref="tableRef"
:data="data"
:tree-config="treeConfig"
:row-config="rowConfig"
@current-change="handleCurrentChange"
>
<slot />
</vxe-table>
</template>
<script>
export default {
props: {
data: Array,
treeConfig: {
type: Object,
default: () => ({
children: 'children',
expandAll: true
})
},
selectedRow: Object
},
watch: {
selectedRow(newRow) {
this.$refs.tableRef?.setCurrentRow(newRow)
}
},
methods: {
handleCurrentChange({ row }) {
this.$emit('update:selectedRow', row)
}
}
}
</script>
html复制<vxe-tree-table
:data="rows"
:tree-config="{ children: 'children' }"
:selected-row.sync="selectedRow"
>
<!-- 设备列(树形节点列) -->
<vxe-column field="device" title="Device" tree-node>
<template #default="{ row }">
<q-icon name="storage" size="12px" />
{{ row.device }}
</template>
</vxe-column>
<!-- 其他数据列... -->
</vxe-tree-table>
tree-config:
children: 指定子节点字段名expandAll: 是否默认展开所有节点trigger: 展开/折叠的触发方式row-config:
isCurrent: 是否高亮当前行isHover: 是否启用悬停效果keyField: 行数据唯一标识字段事件处理:
current-change: 当前行变化事件toggle-tree-expand: 节点展开/折叠事件javascript复制const rawData = [
{
device: 'sda',
partitions: [
{ device: 'sda1', size: '512MB' },
{ device: 'sda2', size: '1GB' }
]
}
]
javascript复制methods: {
buildRows(disks) {
const roots = []
const byDevpath = {}
let rowId = 0
disks.forEach(d => {
const row = {
id: `row_${rowId++}`,
device: d.devpath || d.name,
children: (d.partitions || []).map(p => ({
id: `row_${rowId++}`,
device: p.devpath || p.name,
type: 'partition'
}))
}
byDevpath[d.devpath] = row
})
// 处理父子关系
Object.values(byDevpath).forEach(row => {
if (row.parent && byDevpath[row.parent]) {
byDevpath[row.parent].children.push(row)
} else {
roots.push(row)
}
})
return roots
}
}
问题现象:子节点无法正确展开或显示位置错乱
解决方案:
tree-config.children 指定的字段名是否正确javascript复制// 确保数据结构如下
{
id: '1',
label: '父节点',
children: [
{ id: '1-1', label: '子节点' }
]
}
问题现象:无法正确获取选中行或选择状态不更新
解决方案:
row-config.keyField 配置正确current-change 事件而非 select-changejavascript复制<vxe-table
@current-change="({ row }) => selectedRow = row"
></vxe-table>
当数据量较大时(>1000 节点):
tree-config.lazy 和 load-methodscroll-x 和 scroll-yjavascript复制treeConfig: {
lazy: true,
loadMethod: ({ row }) => {
return loadChildren(row.id)
}
}
| 特性 | vue-table-with-tree-grid | VxeTable |
|---|---|---|
| 安装大小 | ~20KB | ~100KB |
| 树形功能完整性 | 基础功能 | 完整功能 |
| 选择功能可靠性 | 需自定义 | 内置完善 |
| 文档完整性 | 一般 | 优秀 |
| 扩展性 | 有限 | 丰富 |
| 适合场景 | 简单树形展示 | 复杂交互需求 |
选型建议:
javascript复制methods: {
async loadChildren(parentId) {
const res = await api.getChildren(parentId)
return res.data.map(item => ({
...item,
hasChild: item.hasChildren // 标记是否有子节点
}))
}
}
配置懒加载:
javascript复制treeConfig: {
lazy: true,
hasChild: 'hasChild',
loadMethod: this.loadChildren
}
html复制<vxe-column field="device" tree-node>
<template #default="{ row, rowIndex }">
<i :class="row.children ? 'icon-folder' : 'icon-file'"></i>
{{ row.device }}
</template>
</vxe-column>
html复制<vxe-table>
<vxe-colgroup title="基本信息">
<vxe-column field="device" title="设备"></vxe-column>
<vxe-column field="type" title="类型"></vxe-column>
</vxe-colgroup>
<vxe-colgroup title="存储信息">
<vxe-column field="size" title="大小"></vxe-column>
<vxe-column field="used" title="已用"></vxe-column>
</vxe-colgroup>
</vxe-table>
数据规范:
性能优化:
交互设计:
错误处理:
在实际项目中,我最终选择了 VxeTable 方案,因为它提供了更稳定的选择功能和更丰富的扩展选项。特别是在需要支持未来可能的多选和其他高级功能时,VxeTable 的架构设计让后续扩展变得更加容易。对于样式问题,通过深度选择器和合理的作用域样式,可以很好地解决与 Quasar 框架的样式冲突问题。