You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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>

方案优势

  1. 动态适配层级:不管后端返回多少层嵌套数据,组件都会自动递归渲染,无需修改代码
  2. 减少props冗余:展开状态集中管理,不用在多级组件间传递展开状态props
  3. 可配置性强:通过childField参数指定子数据字段,适配不同后端返回结构
  4. 维护成本低:所有行的渲染逻辑集中在一个组件,修改一处即可同步所有层级

如果需要父组件统一控制展开状态(比如批量展开/折叠),可以把expandState提升到TableSection.vue中,通过v-model传递给递归组件。

内容的提问来源于stack exchange,提问作者bodek98

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 04:25:15