Vue3如何实现无限嵌套(可展开行)表格?
动态可展开嵌套表格解决方案
核心思路
放弃静态定义多级子行组件的方式,改用递归组件实现动态嵌套渲染:
- 只需要一个通用行组件,自动识别当前行是否有子数据,递归调用自身渲染子行
- 集中管理所有行的展开状态,避免跨层级传递props
- 通过配置字段名适配后端返回的嵌套数据结构
代码实现
1. 父组件 TableSection.vue
负责初始化表格容器,渲染根层级的递归行组件:
<template> <tbody> <RecursiveTableRow v-for="(row, index) in p.rowsList" :key="row.id || index" :row="row" :headers="p.headers" :child-field="p.tasksTitle" :expand-cell="p.expandCell" :same-cell-titles="p.sameCellTitles" :theme="p.theme" @send-time-log-element="(value) => $emit('sendTimeLogElement', value)" @send-selected-row="sendSelectedRow" /> </tbody> </template> <script setup> import RecursiveTableRow from './RecursiveTableRow.vue'; const p = defineProps({ rowsList: Array, headers: Array, tasksTitle: String, expandCell: Object, sameCellTitles: Array, theme: String }); const emit = defineEmits(['sendTimeLogElement', 'sendSelectedRow']); const sendSelectedRow = (row) => { emit('sendSelectedRow', row); }; </script>
2. 递归行组件 RecursiveTableRow.vue
这是核心组件,负责渲染当前行+递归渲染子行:
<template> <!-- 当前行渲染 --> <tr :class="`${p.theme}-row`"> <!-- 展开/折叠按钮 --> <td v-if="hasChildren"> <button @click="toggleExpand" class="expand-btn"> {{ isExpanded ? '▼' : '▶' }} </button> </td> <!-- 表头对应单元格 --> <td v-for="header in p.headers" :key="header.key"> <!-- 处理合并单元格逻辑 --> <span v-if="p.sameCellTitles.includes(header.key) && p.isParentRow"> {{ p.row[header.key] }} </span> <span v-else> {{ getCellValue(p.row, header.key) }} </span> </td> <!-- 操作栏 --> <td> <button @click="$emit('send-time-log-element', p.row)" class="log-btn"> 时间日志 </button> <input type="checkbox" @change="$emit('send-selected-row', p.row)" class="row-checkbox" > </td> </tr> <!-- 递归渲染子行 --> <template v-if="isExpanded && hasChildren"> <RecursiveTableRow v-for="(childRow, index) in p.row[p.childField]" :key="childRow.id || index" :row="childRow" :headers="p.headers" :child-field="p.childField" :expand-cell="p.expandCell" :same-cell-titles="p.sameCellTitles" :theme="p.theme" :is-parent-row="false" @send-time-log-element="$emit('send-time-log-element', $event)" @send-selected-row="$emit('send-selected-row', $event)" /> </template> </template> <script setup> import { ref, computed } from 'vue'; const p = defineProps({ row: Object, headers: Array, childField: String, // 指定后端返回的子数据字段名,比如tasksTitle expandCell: Object, sameCellTitles: Array, theme: String, isParentRow: { type: Boolean, default: true } }); const emit = defineEmits(['send-time-log-element', 'send-selected-row']); // 集中管理所有行的展开状态(用行唯一标识作为key) const expandState = ref({}); // 判断当前行是否存在子数据 const hasChildren = computed(() => { return Array.isArray(p.row[p.childField]) && p.row[p.childField].length > 0; }); // 当前行的展开状态 const isExpanded = computed(() => { // 优先用id作为唯一标识,没有则序列化行数据 const rowKey = p.row.id || JSON.stringify(p.row); return expandState.value[rowKey] || false; }); // 切换展开/折叠状态 const toggleExpand = () => { const rowKey = p.row.id || JSON.stringify(p.row); expandState.value[rowKey] = !expandState.value[rowKey]; }; // 处理嵌套字段的取值(比如header.key是"user.name"时自动解析) const getCellValue = (row, key) => { return key.split('.').reduce((obj, k) => obj?.[k], row); }; </script> <style scoped> tr { border-bottom: 1px solid #eee; } td { padding: 8px 12px; } .expand-btn { border: none; background: transparent; cursor: pointer; font-size: 14px; } .log-btn { margin-right: 8px; padding: 4px 8px; border: 1px solid #ddd; border-radius: 4px; cursor: pointer; } .row-checkbox { cursor: pointer; } </style>
方案优势
- 动态适配层级:不管后端返回多少层嵌套数据,组件都会自动递归渲染,无需修改代码
- 减少props冗余:展开状态集中管理,不用在多级组件间传递展开状态props
- 可配置性强:通过
childField参数指定子数据字段,适配不同后端返回结构 - 维护成本低:所有行的渲染逻辑集中在一个组件,修改一处即可同步所有层级
如果需要父组件统一控制展开状态(比如批量展开/折叠),可以把expandState提升到TableSection.vue中,通过v-model传递给递归组件。
内容的提问来源于stack exchange,提问作者bodek98
相关产品推荐
相关产品推荐

