Ant Design Vue 表格复杂数据合并单元格

Ant Design Vue 表格复杂数据合并单元格

官方合并效果

官方示例

表头只支持列合并,使用 column 里的 colSpan 进行设置。 表格支持行/列合并,使用 render 里的单元格属性 colSpan 或者 rowSpan 设值为 0 时,设置的表格不会渲染。

vue 复制代码
<template>
  <a-table :columns="columns" :data-source="data" bordered>
    <template slot="name" slot-scope="text">
      <a>{{ text }}</a>
    </template>
  </a-table>
</template>
<script>
// In the fifth row, other columns are merged into first column
// by setting it's colSpan to be 0
const renderContent = (value, row, index) => {
  const obj = {
    children: value,
    attrs: {},
  };
  if (index === 4) {
    obj.attrs.colSpan = 0;
  }
  return obj;
};

const data = [
  {
    key: '1',
    name: 'John Brown',
    age: 32,
    tel: '0571-22098909',
    phone: 18889898989,
    address: 'New York No. 1 Lake Park',
  },
  {
    key: '2',
    name: 'Jim Green',
    tel: '0571-22098333',
    phone: 18889898888,
    age: 42,
    address: 'London No. 1 Lake Park',
  },
  {
    key: '3',
    name: 'Joe Black',
    age: 32,
    tel: '0575-22098909',
    phone: 18900010002,
    address: 'Sidney No. 1 Lake Park',
  },
  {
    key: '4',
    name: 'Jim Red',
    age: 18,
    tel: '0575-22098909',
    phone: 18900010002,
    address: 'London No. 2 Lake Park',
  },
  {
    key: '5',
    name: 'Jake White',
    age: 18,
    tel: '0575-22098909',
    phone: 18900010002,
    address: 'Dublin No. 2 Lake Park',
  },
];

export default {
  data() {
    const columns = [
      {
        title: 'Name',
        dataIndex: 'name',
        customRender: (text, row, index) => {
          if (index < 4) {
            return <a href="javascript:;">{text}</a>;
          }
          return {
            children: <a href="javascript:;">{text}</a>,
            attrs: {
              colSpan: 5,
            },
          };
        },
      },
      {
        title: 'Age',
        dataIndex: 'age',
        customRender: renderContent,
      },
      {
        title: 'Home phone',
        colSpan: 2,
        dataIndex: 'tel',
        customRender: (value, row, index) => {
          const obj = {
            children: value,
            attrs: {},
          };
          if (index === 2) {
            obj.attrs.rowSpan = 2;
          }
          // These two are merged into above cell
          if (index === 3) {
            obj.attrs.rowSpan = 0;
          }
          if (index === 4) {
            obj.attrs.colSpan = 0;
          }
          return obj;
        },
      },
      {
        title: 'Phone',
        colSpan: 0,
        dataIndex: 'phone',
        customRender: renderContent,
      },
      {
        title: 'Address',
        dataIndex: 'address',
        customRender: renderContent,
      },
    ];
    return {
      data,
      columns,
    };
  },
};
</script>

实际项目中实现效果

实现原理

分层说明

  1. 数据预处理

    • 使用prepareData方法按markId字段分组
    • 组内数据按mergeIs字段排序(值为"是"的排在前)
  2. 双层级合并机制

    • 主合并层:相同markId的"名称"列合并
    • 次级合并层:在相同markId组内,连续mergeIs === '是'的"数量"列合并
  3. 合并标识管理

    • 通过rowSpan属性控制行合并数
    • rowSpan=0表示该单元格被合并
    • originalIndex记录原始位置用于合并定位
  4. 动态计数器机制

    • primarySpan跟踪名称列合并跨度
    • secondarySpan跟踪数量列合并跨度
    • 遇到分组边界或状态变化时重置计数器
js 复制代码
{
  markId: "分组标识",  // 用于主合并层级
  mergeIs: "是/否",   // 用于次级合并层级
  name: "显示内容",    // 名称列数据
  num: "数值"         // 数量列数据
}

数据流向示意图

graph TD A[原始数据] --> B{预处理模块} B -->|分组排序| C[结构优化数据] C --> D{合并处理器} D --> E[可合并数据集] E --> F[表格渲染]

表格组件配置

vue 复制代码
<template>
  <section class="console-section-box">
    <div class="con">
      <a-table
        :columns="columns"
        :data-source="tableData"
        :showHeader="true"
        :loading="tableLoading"
        :pagination="pagination"
        :bordered="true"
        :rowKey="
          (record, index) => {
            return index;
          }
        "
        :scroll="{ x: true }"
      >
      </a-table>
    </div>
    <a-back-top />
  </section>
</template>

合并逻辑

js 复制代码
<script>
import { mockData } from '~/mock/index.js';
const productColumn = [
  {
    title: '名称',
    dataIndex: 'name',
    customRender: (value, row, index) => {
      const { rowSpan, originalIndex } = row.nameCellObj || { rowSpan: 1, originalIndex: index };
      const obj = {
        children: value,
        attrs: {}
      };
      if (index === originalIndex) {
        obj.attrs.rowSpan = rowSpan;
        obj.attrs.colSpan = 1;
      }
      return obj;
    },
    align: 'center',
    width: 90
  },
  {
    title: '类型',
    dataIndex: 'type',
    align: 'center',
    width: 100
  },
  {
    title: '数量',
    dataIndex: 'num',
    key: 'num',
    customRender: (value, row, index) => {
      const { rowSpan, originalIndex } = row.numCellObj || { rowSpan: 1, originalIndex: index };
      const obj = {
        children: value,
        attrs: {}
      };
      if (index === originalIndex) {
        obj.attrs.rowSpan = rowSpan;
        obj.attrs.colSpan = 1;
      }
      return obj;
    },
    align: 'center',
    width: 90
  }
];
export default {
  name: '',
  data() {
    return {
      tableLoading: false,
      tableData: [],
      pagination: {
        current: 1, // 当前页码
        pageSize: 10000, // 每页显示条数
        total: 0,
        showTotal: total => `共有 ${total} 条数据` //分页中显示总的数据
      },
      columns: productColumn,
    };
  },
  async mounted() {
    await this.fetchData();
  },
  methods: {
    async fetchData() {
      this.tableLoading = true;
      try {
        const res = await this.XXXX();
        if (res.code === 0) {
          this.tableData = mockData;
          this.pagination.total = res.data.length;
          this.handleCellMerge(this.tableData);
        }
        
      } catch (error) {
        console.error('Error fetching data:', error);
      }
      this.tableLoading = false;
    },
   // 根据数据合并单元格
    handleCellMerge(arr) {
      if (!arr?.length) return;

      const processor = {
        currentMarkId: null,
        currentMergeIs: null,
        primarySpan: 1,
        secondarySpan: 1,

        // 初始化单元格状态
        initialize(row, index) {
          row.nameCellObj = { rowSpan: 1, originalIndex: index };
          row.numCellObj = { rowSpan: 1, originalIndex: index };
        },

        // 主合并逻辑
        processPrimary(index, rows) {
          if (rows[index].markId === this.currentMarkId) {
            this.primarySpan++;
            rows[index - this.primarySpan + 1].nameCellObj.rowSpan = this.primarySpan;
            rows[index].nameCellObj.rowSpan = 0;
            return true;
          }
          this.currentMarkId = rows[index].markId;
          this.primarySpan = 1;
          return false;
        },

        // 次级合并逻辑
        processSecondary(index, rows) {
          if (rows[index].mergeIs === this.currentMergeIs && this.currentMergeIs === '是') {
            this.secondarySpan++;
            rows[index - this.secondarySpan + 1].numCellObj.rowSpan = this.secondarySpan;
            rows[index].numCellObj.rowSpan = 0;
            return true;
          }
          this.currentMergeIs = rows[index].mergeIs;
          this.secondarySpan = 1;
          return false;
        }
      };

      const sortedData = this.prepareData(arr);
      processor.currentMarkId = sortedData[0].markId;
      processor.currentMergeIs = sortedData[0].mergeIs;

      // 单次遍历处理所有合并逻辑
      sortedData.forEach((item, index) => {
        processor.initialize(item, index);
        if (index === 0) return;

        if (processor.processPrimary(index, sortedData)) {
          processor.processSecondary(index, sortedData);
        } else {
          processor.currentMergeIs = item.mergeIs;
        }
      });

      arr.splice(0, arr.length, ...sortedData);
    },
       // 分组排序方法
    prepareData(originData) {
      // 使用Map提高分组性能
      const groups = new Map();
      for (const item of originData) {
        const group = groups.get(item.markId) || [];
        group.push(item);
        groups.set(item.markId, group);
      }

      // 预计算排序权重避免重复计算
      return Array.from(groups.values()).flatMap(group => group.sort((a, b) => (b.mergeIs === '是') - (a.mergeIs === '是')));
    }
  }
};
</script>

mock数据

mock/index.js

js 复制代码
export const mockData = [
  {
    name: '数据A',
    num: '9999999',
    type: 'AAA',
    mergeIs: '是',
    markId: 'ITEM_001'
  },
  {
    name: '数据A',
    num: '9999999',
    type: 'BBB',
    mergeIs: '是',
    markId: 'ITEM_001'
  },
  {
    name: '数据A',
    num: '9999999',
    type: 'CCC',
    mergeIs: '否',
    markId: 'ITEM_001'
  },
  {
    name: '数据A',
    num: '9999999',
    type: 'DDD',
    mergeIs: '否',
    markId: 'ITEM_001'
  },
  {
    name: '数据A',
    num: '9999999',
    type: 'EEE',
    mergeIs: '否',
    markId: 'ITEM_001'
  },
  {
    name: '数据B',
    num: '600',
    type: 'AAA',
    mergeIs: '是',
    markId: 'ITEM_002'
  },
  {
    name: '数据B',
    num: '9999999',
    type: 'BBB',
    mergeIs: '否',
    markId: 'ITEM_002'
  },
  {
    name: '数据B',
    num: '600',
    type: 'CCC',
    mergeIs: '是',
    markId: 'ITEM_002'
  },
  {
    name: '数据B',
    num: '9999999',
    type: 'DDD',
    mergeIs: '否',
    markId: 'ITEM_002'
  },
  {
    name: '数据B',
    num: '9999999',
    type: 'EEE',
    mergeIs: '否',
    markId: 'ITEM_002'
  },
  {
    name: '数据C',
    num: '9999999',
    type: 'AAA',
    mergeIs: '否',
    markId: 'ITEM_003'
  },
  {
    name: '数据C',
    num: '9999999',
    type: 'BBB',
    mergeIs: '否',
    markId: 'ITEM_003'
  },
  {
    name: '数据C',
    num: '9999999',
    type: 'CCC',
    mergeIs: '否',
    markId: 'ITEM_003'
  },
  {
    name: '数据C',
    num: '9999999',
    type: 'DDD',
    mergeIs: '否',
    markId: 'ITEM_003'
  },
  {
    name: '数据C',
    num: '9999999',
    type: 'EEE',
    mergeIs: '否',
    markId: 'ITEM_003'
  }
];

5. 样式

scss 复制代码
<style lang="scss" scoped>
.con {
  min-height: calc(100vh - 160px);
  padding: 24px;
  border-radius: 8px;
  background-color: #fff;
}
.project-info-box {
  display: flex;
  flex-direction: column;
  width: 100%;
  height: 100%;
  padding-bottom: 20px;
}
.project-info {
  width: 100%;
  height: 60px;
  line-height: 60px;
  display: flex;
  justify-content: space-between;
  p {
    margin: 0;
  }
}
</style>
相关推荐
天天扭码1 小时前
零基础 | 入门前端必备技巧——使用 DOM 操作插入 HTML 元素
前端·javascript·dom
咖啡虫2 小时前
css中的3d使用:深入理解 CSS Perspective 与 Transform-Style
前端·css·3d
拉不动的猪2 小时前
设计模式之------策略模式
前端·javascript·面试
旭久2 小时前
react+Tesseract.js实现前端拍照获取/选择文件等文字识别OCR
前端·javascript·react.js
独行soc2 小时前
2025年常见渗透测试面试题-红队面试宝典下(题目+回答)
linux·运维·服务器·前端·面试·职场和发展·csrf
uhakadotcom3 小时前
Google Earth Engine 机器学习入门:基础知识与实用示例详解
前端·javascript·面试
麓殇⊙3 小时前
Vue--组件练习案例
前端·javascript·vue.js
outstanding木槿3 小时前
React中 点击事件写法 的注意(this、箭头函数)
前端·javascript·react.js
会点php的前端小渣渣3 小时前
vue的计算属性computed的原理和监听属性watch的原理(新)
前端·javascript·vue.js
_一条咸鱼_4 小时前
深入解析 Vue API 模块原理:从基础到源码的全方位探究(八)
前端·javascript·面试