Element Plus实现表格单元格内嵌套表格的方法

2026-09-04 15:14:41 6 次阅读

Element Plus 的 el-table 本身支持展开行、树形数据以及自定义单元格内容,因此当业务数据存在“主表 + 明细表”这样的层级关系时,可以直接在单元格中再次嵌套一个 el-table,实现更加直观的数据展示效果。

常见场景包括订单与商品明细、项目与任务列表、用户与操作记录、商品与 SKU 信息等。核心思路是利用 el-table-column 的插槽,在父表格单元格内部放置一个子表格。

一、基本实现思路

假设页面需要展示订单列表,每一行是一个订单,而订单中的商品需要以表格形式展示:

订单编号    用户       商品明细
------------------------------------------------
10001      张三       商品A  2件  100元
                         商品B  1件   50元
------------------------------------------------
10002      李四       商品C  3件  240元
                         商品D  2件  160元

如果直接把商品明细拼接成字符串,数据较多时可读性较差。更合适的方式是在“商品明细”这一列中嵌套一个新的 el-table

数据结构可以设计成:

JavaScript
const tableData = [
  {
    orderNo: '10001',
    userName: '张三',
    amount: 150,
    products: [
      {
        name: '商品A',
        quantity: 2,
        price: 50
      },
      {
        name: '商品B',
        quantity: 1,
        price: 50
      }
    ]
  },
  {
    orderNo: '10002',
    userName: '李四',
    amount: 400,
    products: [
      {
        name: '商品C',
        quantity: 3,
        price: 80
      },
      {
        name: '商品D',
        quantity: 2,
        price: 80
      }
    ]
  }
]

每一条主表数据中的 products 就对应当前订单的子表格数据。

二、使用插槽嵌套 el-table

Vue 3 + Element Plus 中,可以通过 el-table-column 的默认插槽获取当前行数据。

完整示例:

vue

   :data="tableData" border>
    
      prop="orderNo"
      label="订单编号"
      width="150"
    />

    
      prop="userName"
      label="用户"
      width="120"
    />

     label="商品明细">
       #default="scope">
        
          :data="scope.row.products"
          border
          size="small"
          style="width: 100%"
        >
          
            prop="name"
            label="商品名称"
          />

          
            prop="quantity"
            label="数量"
            width="100"
          />

          
            prop="price"
            label="单价"
            width="120"
          />
        
      
    

    
      prop="amount"
      label="订单金额"
      width="120"
    />
  


 setup>
import { ref } from 'vue'

const tableData = ref([
  {
    orderNo: '10001',
    userName: '张三',
    amount: 150,
    products: [
      {
        name: '商品A',
        quantity: 2,
        price: 50
      },
      {
        name: '商品B',
        quantity: 1,
        price: 50
      }
    ]
  },
  {
    orderNo: '10002',
    userName: '李四',
    amount: 400,
    products: [
      {
        name: '商品C',
        quantity: 3,
        price: 80
      },
      {
        name: '商品D',
        quantity: 2,
        price: 80
      }
    ]
  }
])

其中最关键的代码是:

vue
 #default="scope">
   :data="scope.row.products">
    ...
  

scope.row 表示当前父表格行的数据,而 scope.row.products 就是该行对应的子表格数据。

三、给嵌套表格增加展开效果

如果每一行的子数据比较多,直接把完整的子表格显示出来会导致主表高度迅速增加。这种情况下,更推荐使用 Element Plus 的展开行功能。

父表格增加一个展开列:

vue
 :data="tableData" border>
   type="expand">
     #default="scope">
      
        :data="scope.row.products"
        border
        size="small"
      >
        
          prop="name"
          label="商品名称"
        />

        
          prop="quantity"
          label="数量"
          width="100"
        />

        
          prop="price"
          label="单价"
          width="120"
        />
      
    
  

  
    prop="orderNo"
    label="订单编号"
  />

  
    prop="userName"
    label="用户"
  />

  
    prop="amount"
    label="订单金额"
  />

这样默认只显示订单信息,点击左侧展开按钮后才显示对应的商品明细。

这种方案通常比“所有单元格直接嵌套表格”更加适合后台管理系统。

四、在单元格中根据条件显示子表格

实际项目中,并不是每一行都有明细数据。有些数据可能需要显示普通文本,有些数据则需要显示子表格。

可以配合 v-if

vue
 label="详细信息">
   #default="scope">
    
      v-if="scope.row.children?.length"
      :data="scope.row.children"
      border
      size="small"
    >
      
        prop="name"
        label="名称"
      />

      
        prop="status"
        label="状态"
      />
    

     v-else class="empty-text">
      暂无明细
    
  

这样可以避免子数据为空时仍然渲染一个空表格。

五、控制嵌套表格的高度

嵌套表格最大的一个问题是高度。

例如某个订单包含几十条商品记录,如果全部展开,页面会变得非常长。可以给子表格设置最大高度:

vue

  :data="scope.row.products"
  border
  size="small"
  max-height="300"
>

当子表格超过 300px 后,内部会出现滚动区域,而不会无限撑高父表格。

也可以根据数据量动态设置:

vue

  :data="scope.row.products"
  border
  size="small"
  :max-height="scope.row.products.length > 5 ? 300 : undefined"
>

对于数据量较大的后台页面,这种处理能够明显改善页面布局。

六、调整嵌套表格样式

默认情况下,父表格和子表格使用相同的表格样式,视觉层级可能不够明显。

可以为子表格添加自定义 class:

vue

  :data="scope.row.products"
  class="child-table"
  border
  size="small"
>

然后在样式中调整:

CSS
.child-table {
  margin: 8px 0;
}

.child-table :deep(.el-table__header th) {
  background: #f7f7f7;
}

.child-table :deep(.el-table__cell) {
  padding: 6px 0;
}

如果使用

这种写法具有比较清晰的数据层级:

订单表
 ├── 订单编号
 ├── 客户
 ├── 金额
 ├── 状态
 └── 展开
      └── 商品明细表
           ├── 商品名称
           ├── 数量
           ├── 单价
           └── 小计

八、子表格数据从接口动态获取

如果子表数据比较大,不建议在首次加载父表格时把所有明细一次性返回。

例如接口只返回订单:

JSON
[
  {
    "orderNo": "10001",
    "userName": "张三",
    "amount": 150
  }
]

用户展开订单之后,再请求:

GET /api/order/10001/products

可以通过展开事件实现:

vue

  :data="orders"
  @expand-change="handleExpandChange"
>
   type="expand">
     #default="{ row }">
      
        :data="row.products || []"
        v-loading="row.loading"
        border
        size="small"
      >
        
          prop="name"
          label="商品名称"
        />

        
          prop="quantity"
          label="数量"
        />

        
          prop="price"
          label="价格"
        />
      
    
  

  
    prop="orderNo"
    label="订单编号"
  />

  
    prop="userName"
    label="客户"
  />

对应的逻辑:

JavaScript
const handleExpandChange = async (row, expandedRows) => {
  const expanded = expandedRows.some(
    item => item.orderNo === row.orderNo
  )

  if (!expanded || row.productsLoaded) {
    return
  }

  row.loading = true

  try {
    const result = await getOrderProducts(row.orderNo)

    row.products = result.data
    row.productsLoaded = true
  } finally {
    row.loading = false
  }
}

这里的 productsLoaded 很重要。

如果用户反复展开、关闭同一个订单,没有这个状态控制就可能重复请求接口。

九、嵌套表格中的操作按钮

子表格同样可以使用 el-table-column 插槽添加操作:

vue
 label="操作" width="160">
   #default="{ row }">
    
      type="primary"
      link
      @click="handleDetail(row)"
    >
      查看
    

    
      type="danger"
      link
      @click="handleDelete(row)"
    >
      删除
    
  

如果需要同时获取父表格和子表格的数据,可以在事件中传递两个参数:

vue

  type="primary"
  link
  @click="handleDetail(scope.row, productScope.row)"
>
  查看

这样就可以明确知道当前操作属于哪个订单以及哪个商品。

十、常见问题与解决方法

1. 子表格宽度超出父表格

通常可以设置:

CSS
.child-table {
  width: 100%;
}

同时避免给过多列设置固定宽度。如果固定宽度列较多,应该合理配置父表格的整体宽度。

2. 嵌套表格高度太高

优先考虑使用:

vue
 max-height="300">

或者直接采用父表格的展开行设计,只在用户需要时显示子表格。

3. 子表格为空时显示空白区域

可以增加条件:

vue

  v-if="row.products?.length"
  :data="row.products"
>

没有数据时显示:

vue

  v-else
  description="暂无明细"
  :image-size="60"
/>

4. 父子表格字段名称重复

父表格和子表格都使用 namestatus 等字段时,很容易在模板中混淆。

推荐使用:

vue
 #default="{ row: order }">

子表格:

vue
 #default="{ row: product }">

这样代码会更加清晰:

vue
 @click="handleDetail(order, product)">
  查看

5. 子表格修改数据后父表格没有更新

如果子表格中的操作会修改父表格统计信息,例如修改商品数量后订单总金额也发生变化,就需要同步更新父级数据。

例如:

JavaScript
const updateProductQuantity = (order, product, quantity) => {
  product.quantity = quantity

  order.amount = order.products.reduce(
    (total, item) => {
      return total + item.quantity * item.price
    },
    0
  )
}

Vue 3 的响应式系统会跟踪这些对象属性的变化,从而更新对应的页面内容。

十一、什么时候应该使用嵌套表格

Element Plus 嵌套 el-table 并不是所有场景下都适合。

如果父子数据结构比较简单,例如:

部门
 └── 员工

可以考虑使用 Element Plus 的树形表格。

如果子数据需要完整展示多列信息,例如:

订单
 └── 商品名称、数量、价格、优惠、库存、状态

嵌套表格会更加合适。

如果子数据非常多,则建议采用:

父表格
   ↓
展开某一行
   ↓
异步请求子数据
   ↓
显示嵌套表格

这种方式可以减少首屏数据量,也能降低浏览器一次性渲染大量 DOM 的压力。

十二、性能优化建议

当父表格和子表格数据量都比较大时,嵌套表格很容易产生大量 DOM 节点,因此需要注意性能。

首先,不要默认渲染所有子表格,可以使用展开行控制。

其次,子数据可以采用懒加载:

JavaScript
if (!row.productsLoaded) {
  await loadProducts(row)
}

再次,对于特别大的明细列表,可以进一步使用分页:

订单
  └── 商品明细
       ├── 第1页
       ├── 第2页
       └── 第3页

而不是一次性加载全部数据。

另外,如果父表格有大量行,还应该避免在模板中执行复杂计算。例如不要频繁调用非常耗时的方法:

vue
{{ calculateAmount(row) }}

对于复杂数据,可以在接口层完成计算,或者通过 computed、缓存等方式减少重复计算。

十三、完整方案选择建议

根据实际业务需求,可以按照下面的方式选择:

场景推荐方案
少量子数据直接展示单元格嵌套 el-table
子数据较多展开行 + 嵌套 el-table
子数据需要点击后加载展开行 + 异步请求
父子数据层级明显树形表格
子数据数量非常大展开行 + 分页
子数据结构复杂独立详情区域或详情页

其中最常用的方案是“el-table-column 插槽 + el-table”。

核心代码其实只有两层:

vue
 label="明细">
   #default="{ row }">
     :data="row.children">
      ...
    
  

如果数据量较大,则进一步改成:

vue
 type="expand">
   #default="{ row }">
     :data="row.children">
      ...
    
  

前者适合直接展示少量关联数据,后者更适合订单明细、操作记录等实际后台管理场景。结合异步加载、最大高度和分页机制后,可以在保持页面结构清晰的同时,有效控制渲染成本和接口请求量。