Vue+elementUI的el-table组件,合并单元格,勾选之后,复制、新增、删除、批量复制、批量删除功能

'# Vue+elementUI的el-table组件,合并单元格,勾选之后,复制、新增、删除、批量复制、批量删除功能

一、背景与问题

在企业级应用开发中,el-table组件是数据展示的核心组件之一。当需要处理复杂数据展示需求时,常会遇到以下问题:

  1. 合并单元格需求:需要将同一组数据中的相同字段合并展示,例如订单列表中同一客户的多条订单合并展示
  2. 多选操作需求:需要支持勾选行后执行复制、删除等操作
  3. 批量操作需求:需要支持批量复制、批量删除等操作
  4. 数据一致性需求:需要保证操作后的数据状态与UI同步

传统开发中,这些问题需要结合el-table的row-class-name、cell-class-name、scope属性等特性进行组合实现,但容易出现性能问题、状态不一致等问题。

二、基本原理

1. 合并单元格原理

elementUI的el-table通过row-class-namecell-class-name控制单元格样式,但真正的合并需要通过rowspancolspan属性实现。其核心原理是:

  • 通过遍历数据,计算每个单元格的合并行数和列数
  • 通过rowspancolspan属性控制单元格的合并行为
  • 需要维护一个标记数组,记录哪些单元格需要合并

2. 多选操作原理

elementUI的el-table支持多选功能,通过type="selection"列实现。其核心原理是:

  • 通过@selection-change事件获取选中行数据
  • 通过ref获取表格实例,调用toggleRowSelection方法控制行的选中状态
  • 需要维护一个选中行的集合,保证数据状态与UI同步

3. 批量操作原理

批量操作需要结合Vue的响应式系统和数组的变异方法,其核心原理是:

  • 通过v-model绑定选中行数据
  • 使用filtermap等数组方法处理数据
  • 需要处理数据的深拷贝,避免引用污染

三、环境准备

确保项目中已安装以下依赖:

npm install element-plus

在Vue3项目中需要引入:

import { ElTable, ElTableColumn, ElSelect, ElOption, ElInput } from 'element-plus'

四、核心实现

1. 合并单元格实现

<template>
  <el-table
    :data="tableData"
    border
    :row-class-name="getRowClassName"
    :cell-class-name="getCellClassName"
    style="width: 100%"
  >
    <el-table-column type="index" label="序号" width="80"></el-table-column>
    <el-table-column prop="client" label="客户名称" width="150"></el-table-column>
    <el-table-column prop="product" label="产品名称" width="150"></el-table-column>
    <el-table-column prop="quantity" label="数量" width="100"></el-table-column>
    <el-table-column prop="price" label="单价" width="100"></el-table-column>
    <el-table-column prop="amount" label="金额" width="100"></el-table-column>
  </el-table>
</template>

<script>
export default {
  data() {
    return {
      tableData: [
        { id: 1, client: '客户A', product: '产品1', quantity: 2, price: 100, amount: 200, type: 'detail' },
        { id: 2, client: '客户A', product: '产品2', quantity: 3, price: 200, amount: 600, type: 'detail' },
        { id: 3, client: '客户B', product: '产品3', quantity: 1, price: 300, amount: 300, type: 'detail' },
        { id: 4, client: '客户B', product: '产品4', quantity: 4, price: 400, amount: 1600, type: 'detail' },
        { id: 5, client: '客户C', product: '产品5', quantity: 5, price: 500, amount: 2500, type: 'detail' }
      ],
      mergedRows: []
    }
  },
  methods: {
    getRowClassName({ row, index }) {
      if (row.type === 'header') {
        return 'header-row'
      }
      return ''
    },
    getCellClassName({ row, column, rowIndex, columnIndex }) {
      const client = row.client
      const cellIndex = columnIndex

      // 处理客户名称列的合并
      if (column.property === 'client') {
        const isHeader = this.mergedRows.includes(row.id)
        if (isHeader) {
          return 'merged-cell'
        }
        return ''
      }

      // 处理其他列的合并
      if (column.property !== 'client' && this.mergedRows.includes(row.id)) {
        return 'merged-cell'
      }
      return ''
    }
  }
}
</script>

<style scoped>
.header-row {
  background-color: #f5f7fa;
}

.merged-cell {
  background-color: #e4e7ed;
}
</style>

关键代码解释:

  1. getRowClassName方法用于设置表头行的样式
  2. getCellClassName方法用于设置合并单元格的样式
  3. 通过遍历数据计算需要合并的行
  4. 使用mergedRows数组记录需要合并的行ID

2. 多选操作实现

<template>
  <el-table
    ref="tableRef"
    :data="tableData"
    border
    @selection-change="handleSelectionChange"
    style="width: 100%"
  >
    <el-table-column type="selection" width="55"></el-table-column>
    <el-table-column prop="client" label="客户名称" width="150"></el-table-column>
    <el-table-column prop="product" label="产品名称" width="150"></el-table-column>
    <el-table-column prop="quantity" label="数量" width="100"></el-table-column>
    <el-table-column prop="price" label="单价" width="100"></el-table-column>
    <el-table-column prop="amount" label="金额" width="100"></el-table-column>
  </el-table>
  <div style="margin-top: 20px">
    <el-button @click="copySelected">复制选中</el-button>
    <el-button @click="deleteSelected">删除选中</el-button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      tableData: [
        { id: 1, client: '客户A', product: '产品1', quantity: 2, price: 100, amount: 200 },
        { id: 2, client: '客户A', product: '产品2', quantity: 3, price: 200, amount: 600 },
        { id: 3, client: '客户B', product: '产品3', quantity: 1, price: 300, amount: 300 },
        { id: 4, client: '客户B', product: '产品4', quantity: 4, price: 400, amount: 1600 },
        { id: 5, client: '客户C', product: '产品5', quantity: 5, price: 500, amount: 2500 }
      ],
      selectedRows: []
    }
  },
  methods: {
    handleSelectionChange(rows) {
      this.selectedRows = rows
    },
    copySelected() {
      const copiedRows = this.selectedRows.map(row => ({ ...row }))
      this.tableData = [...this.tableData, ...copiedRows]
      this.$refs.tableRef.toggleRowSelection(this.selectedRows)
    },
    deleteSelected() {
      this.tableData = this.tableData.filter(row => !this.selectedRows.includes(row))
    }
  }
}
</script>

关键代码解释:

  1. 使用@selection-change事件获取选中行数据
  2. toggleRowSelection方法用于控制行的选中状态
  3. 使用map方法进行深拷贝,避免引用污染
  4. 使用filter方法删除选中行

3. 批量操作实现

<template>
  <el-table
    ref="tableRef"
    :data="tableData"
    border
    @selection-change="handleSelectionChange"
    style="width: 100%"
  >
    <el-table-column type="selection" width="55"></el-table-column>
    <el-table-column prop="client" label="客户名称" width="150"></el-table-column>
    <el-table-column prop="product" label="产品名称" width="150"></el-table-column>
    <el-table-column prop="quantity" label="数量" width="100"></el-table-column>
    <el-table-column prop="price" label="单价" width="100"></el-table-column>
    <el-table-column prop="amount" label="金额" width="100"></el-table-column>
  </el-table>
  <div style="margin-top: 20px">
    <el-button @click="batchCopy">批量复制</el-button>
    <el-button @click="batchDelete">批量删除</el-button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      tableData: [
        { id: 1, client: '客户A', product: '产品1', quantity: 2, price: 100, amount: 200 },
        { id: 2, client: '客户A', product: '产品2', quantity: 3, price: 200, amount: 600 },
        { id: 3, client: '客户B', product: '产品3', quantity: 1, price: 300, amount: 300 },
        { id: 4, client: '客户B', product: '产品4', quantity: 4, price: 400, amount: 1600 },
        { id: 5, client: '客户C', product: '产品5', quantity: 5, price: 500, amount: 2500 }
      ],
      selectedRows: []
    }
  },
  methods: {
    handleSelectionChange(rows) {
      this.selectedRows = rows
    },
    batchCopy() {
      const copiedRows = this.selectedRows.map(row => ({ ...row }))
      this.tableData = [...this.tableData, ...copiedRows]
      this.$refs.tableRef.toggleRowSelection(this.selectedRows)
    },
    batchDelete() {
      this.tableData = this.tableData.filter(row => !this.selectedRows.includes(row))
    }
  }
}
</script>

关键代码解释:

  1. 使用toggleRowSelection方法控制行的选中状态
  2. 使用map方法进行深拷贝,避免引用污染
  3. 使用filter方法删除选中行
  4. 批量操作需要处理大量数据时,注意性能优化

五、完整案例

1. 案例需求

实现一个订单管理界面,包含:

  • 合并客户名称列
  • 支持多选操作
  • 支持复制、删除、批量复制、批量删除
  • 支持新增行

2. 完整代码

<template>
  <div style="padding: 20px">
    <el-button type="primary" @click="addRow">新增行</el-button>
    <el-button type="success" @click="batchCopy">批量复制</el-button>
    <el-button type="danger" @click="batchDelete">批量删除</el-button>
    <el-table
      ref="tableRef"
      :data="tableData"
      border
      @selection-change="handleSelectionChange"
      @row-click="handleRowClick"
      style="width: 100%; margin-top: 20px"
    >
      <el-table-column type="selection" width="55"></el-table-column>
      <el-table-column prop="client" label="客户名称" width="150"></el-table-column>
      <el-table-column prop="product" label="产品名称" width="150"></el-table-column>
      <el-table-column prop="quantity" label="数量" width="100"></el-table-column>
      <el-table-column prop="price" label="单价" width="100"></el-table-column>
      <el-table-column prop="amount" label="金额" width="100"></el-table-column>
    </el-table>
  </div>
</template>

<script>
export default {
  data() {
    return {
      tableData: [
        { id: 1, client: '客户A', product: '产品1', quantity: 2, price: 100, amount: 200, type: 'detail' },
        { id: 2, client: '客户A', product: '产品2', quantity: 3, price: 200, amount: 600, type: 'detail' },
        { id: 3, client: '客户B', product: '产品3', quantity: 1, price: 300, amount: 300, type: 'detail' },
        { id: 4, client: '客户B', product: '产品4', quantity: 4, price: 400, amount: 1600, type: 'detail' },
        { id: 5, client: '客户C', product: '产品5', quantity: 5, price: 500, amount: 2500, type: 'detail' }
      ],
      selectedRows: [],
      mergedRows: []
    }
  },
  mounted() {
    this.calculateMergedRows()
  },
  methods: {
    calculateMergedRows() {
      this.mergedRows = []
      const clients = []
      const rowIds = []

      this.tableData.forEach((row, index) => {
        if (row.type === 'detail') {
          if (clients.length > 0) {
            const lastClient = clients[clients.length - 1]
            if (lastClient.client === row.client) {
              rowIds[rowIds.length - 1] = row.id
            } else {
              clients.push(row)
              rowIds.push(row.id)
            }
          } else {
            clients.push(row)
            rowIds.push(row.id)
          }
        }
      })

      this.mergedRows = rowIds
    },
    handleSelectionChange(rows) {
      this.selectedRows = rows
    },
    handleRowClick(row) {
      if (row.type === 'header') {
        this.$refs.tableRef.toggleRowSelection(row)
      }
    },
    addRow() {
      const newId = this.tableData.length + 1
      const newClient = this.tableData[this.tableData.length - 1].client
      this.tableData.push({
        id: newId,
        client: newClient,
        product: '新产品',
        quantity: 1,
        price: 100,
        amount: 100,
        type: 'detail'
      })
      this.calculateMergedRows()
    },
    batchCopy() {
      const copiedRows = this.selectedRows.map(row => ({ ...row }))
      this.tableData = [...this.tableData, ...copiedRows]
      this.$refs.tableRef.toggleRowSelection(this.selectedRows)
    },
    batchDelete() {
      this.tableData = this.tableData.filter(row => !this.selectedRows.includes(row))
      this.calculateMergedRows()
    }
  }
}
</script>

<style scoped>
.el-table {
  border: 1px solid #ebeef5;
}
</style>

关键代码解释:

  1. calculateMergedRows方法计算需要合并的行
  2. handleRowClick方法处理行点击事件,支持头行和明细行的切换
  3. addRow方法新增行时重新计算合并行
  4. 批量操作后需要重新计算合并行

六、源码解析

1. 合并单元格实现细节

calculateMergedRows() {
  this.mergedRows = []
  const clients = []
  const rowIds = []

  this.tableData.forEach((row, index) => {
    if (row.type === 'detail') {
      if (clients.length > 0) {
        const lastClient = clients[clients.length - 1]
        if (lastClient.client === row.client) {
          rowIds[rowIds.length - 1] = row.id
        } else {
          clients.push(row)
          rowIds.push(row.id)
        }
      } else {
        clients.push(row)
        rowIds.push(row.id)
      }
    }
  })

  this.mergedRows = rowIds
}

这个方法遍历所有行,计算需要合并的行ID。通过维护一个客户端列表和行ID列表,当遇到相同客户名称时,将当前行ID加入到前一个客户的行ID列表中。最终生成的mergedRows数组包含所有需要合并的行ID。

2. 多选操作实现细节

handleSelectionChange(rows) {
  this.selectedRows = rows
}

这个方法通过@selection-change事件获取选中行,将选中行存储在selectedRows中,用于后续的复制和删除操作。

3. 批量操作实现细节

batchCopy() {
  const copiedRows = this.selectedRows.map(row => ({ ...row }))
  this.tableData = [...this.tableData, ...copiedRows]
  this.$refs.tableRef.toggleRowSelection(this.selectedRows)
}

这个方法通过map创建深拷贝,然后将复制的行添加到数据源中,最后调用toggleRowSelection方法保持选中状态。

七、进阶使用

1. 动态合并单元格

calculateMergedRows() {
  this.mergedRows = []
  const clients = []
  const rowIds = []

  this.tableData.forEach((row, index) => {
    if (row.type === 'detail') {
      if (clients.length > 0) {
        const lastClient = clients[clients.length - 1]
        if (lastClient.client === row.client) {
          rowIds[rowIds.length - 1] = row.id
        } else {
          clients.push(row)
          rowIds.push(row.id)
        }
      } else {
        clients.push(row)
        rowIds.push(row.id)
      }
    }
  })

  this.mergedRows = rowIds
}

这个方法可以动态计算需要合并的行,当数据变化时重新计算。

2. 新增行时自动合并

addRow() {
  const newId = this.tableData.length + 1
  const newClient = this.tableData[this.tableData.length - 1].client
  this.tableData.push({
    id: newId,
    client: newClient,
    product: '新产品',
    quantity: 1,
    price: 100,
    amount: 100,
    type: 'detail'
  })
  this.calculateMergedRows()
}

新增行后立即重新计算合并行,确保合并状态正确。

3. 批量操作时的状态保持

batchDelete() {
  this.tableData = this.tableData.filter(row => !this.selectedRows.includes(row))
  this.calculateMergedRows()
}

删除行后重新计算合并行,确保合并状态正确。

八、性能与工程实践

1. 性能优化

  1. 虚拟滚动:对于大数据量的表格,使用虚拟滚动技术减少DOM节点数量
  2. 懒加载:对大量数据进行分页加载,避免一次性加载过多数据
  3. 防抖处理:对于频繁触发的批量操作,使用防抖函数减少不必要的计算

2. 状态管理

  1. 使用ref获取表格实例,避免直接操作DOM
  2. 使用v-model绑定选中行数据,确保数据一致性
  3. 对关键数据进行深拷贝,避免引用污染

3. 异常处理

  1. 对数据进行校验,确保数据完整性
  2. 对操作进行防重处理,避免重复操作
  3. 对错误进行捕获,防止程序崩溃

九、常见问题与踩坑

1. 合并单元格错误

问题:合并单元格时出现错位

原因:合并行的计算逻辑错误,导致行索引不正确

解决方法:仔细检查合并行的计算逻辑,确保索引正确

2. 多选操作异常

问题:多选操作后数据状态不一致

原因:未正确处理数据的深拷贝

解决方法:使用map创建深拷贝,避免引用污染

3. 批量操作性能问题

问题:大数据量下批量操作导致页面卡顿

原因:频繁操作DOM节点导致性能问题

解决方法:使用虚拟滚动、分页等技术优化性能

十、最佳实践

1. 合并单元格的最佳实践

  • 使用rowspancolspan属性实现合并
  • 通过遍历数据计算需要合并的行
  • 使用标记数组记录需要合并的行

2. 多选操作的最佳实践

  • 使用@selection-change事件获取选中行
  • 使用toggleRowSelection方法控制行的选中状态
  • 使用map创建深拷贝,避免引用污染

3. 批量操作的最佳实践

  • 使用数组的变异方法处理数据
  • 对关键数据进行深拷贝
  • 确保操作后的数据状态与UI同步

十一、总结

通过本文的深入分析,我们了解了在Vue+elementUI的el-table组件中实现合并单元格、多选操作、批量操作的原理和实现方法。在实际开发中,需要注意以下几点:

  1. 合并单元格需要通过rowspancolspan属性实现,需要仔细计算合并行
  2. 多选操作需要使用@selection-change事件获取选中行,并通过toggleRowSelection方法控制状态
  3. 批量操作需要使用数组的变异方法处理数据,并注意深拷贝避免引用污染
  4. 性能优化需要考虑虚拟滚动、分页等技术,避免大数据量导致的性能问题
  5. 异常处理需要对数据进行校验,确保数据完整性

在实际项目中,这种方案适用于需要展示分组数据的表格,但需要注意在大数据量场景下进行性能优化。通过合理使用这些技术,可以创建出功能完善、性能良好的数据展示组件。

评论已关闭

推荐阅读

AIGC实战——Transformer模型
2024年12月01日
Socket TCP 和 UDP 编程基础(Python)
2024年11月30日
python , tcp , udp
如何使用 ChatGPT 进行学术润色?你需要这些指令
2024年12月01日
AI
最新 Python 调用 OpenAi 详细教程实现问答、图像合成、图像理解、语音合成、语音识别(详细教程)
2024年11月24日
ChatGPT 和 DALL·E 2 配合生成故事绘本
2024年12月01日
omegaconf,一个超强的 Python 库!
2024年11月24日
【视觉AIGC识别】误差特征、人脸伪造检测、其他类型假图检测
2024年12月01日
[超级详细]如何在深度学习训练模型过程中使用 GPU 加速
2024年11月29日
Python 物理引擎pymunk最完整教程
2024年11月27日
MediaPipe 人体姿态与手指关键点检测教程
2024年11月27日
深入了解 Taipy:Python 打造 Web 应用的全面教程
2024年11月26日
基于Transformer的时间序列预测模型
2024年11月25日
Python在金融大数据分析中的AI应用(股价分析、量化交易)实战
2024年11月25日
AIGC Gradio系列学习教程之Components
2024年12月01日
Python3 `asyncio` — 异步 I/O,事件循环和并发工具
2024年11月30日
llama-factory SFT系列教程:大模型在自定义数据集 LoRA 训练与部署
2024年12月01日
Python 多线程和多进程用法
2024年11月24日
Python socket详解,全网最全教程
2024年11月27日
python之plot()和subplot()画图
2024年11月26日
理解 DALL·E 2、Stable Diffusion 和 Midjourney 工作原理
2024年12月01日