【前端】ant-design-vue 表格「行、列都动态」的处理方案

ant-design-vue 表格「行、列都动态」的处理方案

场景:行是「行业」,列是「统计时间(月份)」,两者都由接口数据决定,前端需要动态拼装表头并填充数据。


一、问题本质

传统 a-table 写法是列固定、行动态:

vue 复制代码
<a-table :columns="columns" :data-source="list" />

columns 是写死的常量,data-source 是接口返回的数组。

而本例中:

  • 行 :行业列表不确定(工业、电力热力......),由接口 xm_gjzbfx_zdgyydl_hy 返回;
  • 列 :统计月份数量不确定(1~12 月任意区间),由接口 xm_gjzbfx_zdgyydl 返回的数据中出现过哪些 tjsj 决定。

因此不能写死列,必须根据数据反推表头。


二、核心思路

接口返回的是**长表(纵向)**结构,一行 = 一个「行业 + 月份」的组合:

js 复制代码
[
  { hymc: '工业',   tjsj: '2024-01', ydl: 123456, ydl_zzl: 5.21 },
  { hymc: '工业',   tjsj: '2024-02', ydl: 234567, ydl_zzl: 6.32 },
  { hymc: '电力...',  tjsj: '2024-01', ydl: 345678, ydl_zzl: 1.10 },
]

而表格需要的是宽表(横向):一行 = 一个行业,各月份横向铺开。

整个转换分三步:

复制代码
接口长表数据
   │
   ├─ ① 按 tjsj 分组(groupBy)  →  得到「列」的定义顺序
   │
   ├─ ② 每组生成一个父列,列下挂 2 个子列(总量 / 增长率)
   │       dataIndex 用「下标」拼接:ydl0、ydl_zzl0、ydl1、ydl_zzl1 ...
   │
   └─ ③ 把每个行业的数值写回「对应行的对象」上,属性名与 dataIndex 严格同名
          row.ydl1 = 234567  ←→  column.dataIndex = 'ydl1'

关键点:columns[i].dataIndex 与 row[dataIndex] 靠「同名」建立映射关系,ant-table 就是靠这个自动取值。所以动态列的本质是:

用「稳定的花名」(如 ydl0、ydl_zzl0)代替「真实业务字段名」,在生成列的同时,把数据按同样的花名写入行对象。


三、代码逐段拆解

1. 表头第一列固定:行维度

js 复制代码
this.zdhyColumns = [
  {
    title: this.zdgyydYear,   // 标题直接用当前选中年份
    dataIndex: 'mc',          // 行维度字段:行业名称
    key: 'mc',
    width: 200,
    fixed: 'left',            // 动态列较多时必须固定左侧,否则横向滚动后看不懂行是什么
  },
];

注意:每次刷新都要整体重建 zdhyColumns(而不是 push 到旧数组里),否则切换年份后会不断叠加历史列。

2. 先查「行」:确定行业列表及顺序

js 复制代码
this.zdhyData = [];
await FormBoListApi.queryForJson('xm_gjzbfx_zdgyydl_hy', params).then(res => {
  if (res.success) {
    res.data.forEach(item => {
      if (item.hymc) this.zdhyData.push({ mc: item.hymc });
    });

    // 把「工业」提到第一行,方便阅读
    if (this.zdhyData.length > 0) {
      const gy = this.zdhyData.find(item => item.mc === '工业');
      this.zdhyData = this.zdhyData.filter(item => item.mc !== '工业');
      this.zdhyData.unshift(gy);
    }
  }
});

此时 zdhyData 只有 mc 一个字段,其余字段在第 4 步补上。

3. 再查「列」:按统计时间分组并生成列

js 复制代码
const list = this.groupBy(data);   // 相同 tjsj 归为一组

list.forEach((val, index) => {
  const columns = {
    title: val.tjsj,               // 父表头:2024-01
    width: 260,
    children: [                    // 子表头:总量 / 增长率
      {
        title: '总量(万千瓦时)',
        dataIndex: `ydl${index}`,  // ← 用分组下标生成唯一 key
        width: 130,
        ellipsis: true,
        scopedSlots: { customRender: 'ydl' },
      },
      {
        title: '增长率(%)',
        dataIndex: `ydl_zzl${index}`,
        width: 130,
        ellipsis: true,
        scopedSlots: { customRender: 'ydl_zb' },
      },
    ],
  };
  this.zdhyColumns.push(columns);
});

groupBy 实现(顺序即 Object.values 的插入顺序,等价于「数据首次出现的顺序」):

js 复制代码
groupBy(resData) {
  const dataInfo = {};
  resData.forEach(item => {
    const { tjsj } = item;
    if (!dataInfo[tjsj]) dataInfo[tjsj] = { tjsj, child: [] };
    dataInfo[tjsj].child.push(item);
  });
  return Object.values(dataInfo);
}

4. 数据回填:把值写进对应行的同名属性

js 复制代码
if (val.child.length > 0) {
  val.child.forEach(val1 => {
    this.zdhyData.forEach(val2 => {
      if (val2.mc === val1.hymc) {                         // 行、列交叉定位
        val2[`ydl${index}`] = val1.ydl ? val1.ydl.toLocaleString() : '--';
        val2[`ydl_zzl${index}`] = val1.ydl_zzl ? val1.ydl_zzl.toFixed(2) : '--';
      }
    });
  });
}

这里 index 必须与第 3 步生成 dataIndex 时的 index 完全一致,否则列与值会错位。

5. 最后一列自适应,消除右侧空白

js 复制代码
// 当 columns 为动态时,给最后一个项设置自适应宽度,防止出现空白间隙
this.zdhyColumns[this.zdhyColumns.length - 1].children[0].width = '';
this.zdhyColumns[this.zdhyColumns.length - 1].children[1].width = '';

动态列总宽常常凑不满容器,最后一列去掉固定宽度可让表格铺满。

若这里把父列 width 也一起置空,记得同步修正 scrollX 的计算逻辑(见下)。

6. 动态计算 scroll.x,配合固定列

js 复制代码
computed: {
  scrollX() {
    // 动态获取 scrollX,防止数组固定列的大小变化
    return this.zdhyColumns.reduce((pre, cur) => pre + cur.width, 0);
  },
}

模板中:

vue 复制代码
<a-table
  bordered
  :columns="zdhyColumns"
  :data-source="zdhyData"
  class="table-b mT10"
  :pagination="false"
  :scroll="{ x: scrollX, y: 800 }"
>
  <template slot="ydl" slot-scope="text">
    <span>{{ text || '--' }}</span>
  </template>
  <template slot="ydl_zb" slot-scope="text">
    <span>{{ text || '--' }}%</span>
  </template>
</a-table>

注意两点:

  1. 只要用了 fixed: 'left',:scroll="{ x }" 就必须给,否则固定列不生效或与内容列错位。
  2. scrollX 只累加顶层列 的 width,所以父列 width(260)应当等于其子列宽度之和(130 + 130),保持一致才不出误差。

7. 自定义渲染:给不同列挂不同模板

动态列无法为每一列单独写 render,用 scopedSlots.customRender 做同类列的共享模板:

  • 总量列 → customRender: 'ydl',模板输出 {``{ text || '--' }}
  • 增长率列 → customRender: 'ydl_zb',模板输出 {``{ text || '--' }}%(补百分比符号)

这样几十个动态列复用同一个插槽,不必逐列定义。


四、完整数据流

复制代码
用户切换 年份/始月/终月 (zdgyYdlHandleChange)
        │
        ▼
getGjzbfxZdgyYdl()
        │
        ├─ 重置 zdhyColumns(仅保留 mc 列)、zdhyData = []
        │
        ├─ 请求 xm_gjzbfx_zdgyydl_hy  →  得到行业列表 → 造「行」+ 工业置顶
        │
        └─ 请求 xm_gjzbfx_zdgyydl     →  groupBy(tjsj)
                                            │
                                   遍历分组 index
                                            │
                              ┌─────────────┴─────────────┐
                              ▼                           ▼
                      push 动态列到 columns        把值写入对应 row
                    (dataIndex = ydl{index})   (row['ydl'+index] = xx)
                              │                           │
                              └─────────────┬─────────────┘
                                            ▼
                              最后一列 width 置空 + scrollX 重算
                                            ▼
                                    a-table 重新渲染

五、可复用模板

剥离业务后的通用写法:

js 复制代码
data() {
  return {
    dynamicColumns: [],
    dynamicData: [],
    dynamicX: 0,
  };
},
methods: {
  async loadDynamicTable() {
    const rows = await fetchRows();   // [{ name, time, value }]
    const cols = await fetchCols();    // 备用:也可直接从 rows 推导时间维度
    const groups = this.groupBy(rows, 'time');

    // 1) 行维度列
    const columns = [
      { title: '名称', dataIndex: 'name', key: 'name', width: 160, fixed: 'left' },
    ];

    // 2) 行数据(先去重)
    const dataMap = {};
    rows.forEach(r => {
      if (!dataMap[r.name]) dataMap[r.name] = { name: r.name };
    });
    const dataSource = Object.values(dataMap);

    // 3) 动态列 + 值回填
    groups.forEach((group, i) => {
      const colKey = `value_${i}`;
      columns.push({
        title: group.key,
        width: 200,
        children: [
          {
            key: colKey,
            title: '数值',
            dataIndex: colKey,
            ellipsis: true,
            scopedSlots: { customRender: 'value' },
          },
        ],
      });
      group.child.forEach(item => {
        const target = dataMap[item.name];
        if (target) target[colKey] = item.value ?? '--';
      });
    });

    // 4) 末列自适应
    if (columns.length > 1) {
      const last = columns[columns.length - 1];
      (last.children || [last]).forEach(c => { c.width = ''; });
    }

    this.dynamicColumns = columns;
    this.dynamicData = dataSource;
    this.dynamicX = columns.reduce((s, c) => s + (Number(c.width) || 0), 0);
  },

  groupBy(list, key) {
    const map = {};
    list.forEach(item => {
      const k = item[key];
      if (!map[k]) map[k] = { key: k, child: [] };
      map[k].child.push(item);
    });
    return Object.values(map);
  },
},

六、踩坑清单

现象 原因 处理
切换筛选条件后列越来越多 只在旧数组上 push,未重置 每次请求前重建 columns 数组
数值与表头错位 生成 dataIndex 的下标与回填时的下标不一致 用同一个 index 变量,或改用 tjsj 作为稳定 key(ydl_${tjsj})
某些单元格空白 行的属性名与 dataIndex 不同名 保证 row[col.dataIndex] 存在,用 ?? / `
固定列错位 / 滚动异常 用了 fixed 但未传 scroll.x 补 :scroll="{ x: scrollX }",并随列变化重算
表格右侧有空白 动态列总宽不足 末列 width 置为 '' 自适应
单元格无值也不显示占位 未挂 scopedSlots 为同类列统一挂 customRender 插槽
Vue 控制台 key 警告 动态子列未设 key 每个动态列补上唯一 key
unshift(undefined) find('工业') 未命中时返回 undefined 先判空再 unshift:if (gy) this.zdhyData.unshift(gy)
数据量大时渲染卡顿 列 × 行 组合过多 开 ellipsis、限制查询区间、必要时虚拟滚动

七、小结

动态行列表格的三条铁律:

  1. 列与数据同源 :dataIndex 与行对象属性名必须由同一套规则生成,口径一处定义、两处使用。
  2. 每次全量重建 :columns 和 data-source 都要整体替换,避免残留脏数据。
  3. 固定列就必给 scroll.x ,且 scroll.x 需按列宽实时计算、末列自适应收尾,才能兼顾「列多可横滚」与「铺满容器」。
相关推荐
szarron2 小时前
VNA6 便携式矢量网络分析仪|1MHz‑6.3GHz 手持 VNA 真实应用场景全解析
前端·嵌入式硬件·信息可视化·数据挖掘·数据分析
Reisentyan2 小时前
B站首页,点击视频卡片之后首页自动刷新的问题
前端
默_笙2 小时前
🚕 缺料就出门买:给 RAG 装上"信息够不够"的判断力
前端·javascript
Liora_Yvonne2 小时前
同样用 Element Plus,为什么你的后台总有一股“模板味”?
前端
葡萄城技术团队2 小时前
用 Sheet 页导航视图管理复杂 SpreadJS 工作簿
前端
志尊宝2 小时前
Vue3 零基础每日笔记(054):Pinia 三件套详解——state / getters / actions 全搞懂
前端·javascript·vue.js·vue·前端开发
Caroline5162 小时前
GPT Image 2.5还能这么玩!一套提示词生成IP吉祥物,附完整案例
前端·gpt·tcp/ip
ym hyd 1112 小时前
慢性病精细化管理平台源码 Java+SpringBoot+Vue3 前后分离
java·vue.js·spring boot·毕设
wangyadong3172 小时前
# el-table 复选框无法选中问题记录
前端·javascript·vue.js