从零到一实现复杂表格需求(antd table 合并行 合并列)

很多小伙伴对于产品提出的陌生需求就“麻爪”。其实如何“解题”很重要,只要掌握正确的思考方式真的很简单。

效果展示

在这里插入图片描述

GIF 转换完比较糊

请添加图片描述

环境

antd@2.x 部分写法与本文不一样,但是思路是一样的

包名版本
react^16.14.0
antd4.22

分析需求

官方文档[Antd-Tabel]

  • 涉及 列左侧固定
    • columns.fixed = left
  • 涉及 table 横纵坐标滚动
    • table.scroll = { x:1,y:600 }
  • 涉及 table 表头列合并
    • columns.colSpan = 2
  • 涉及 table 行合并
    • columns.onCell(函数回调中处理)

细心的小伙伴可以在官方文档中找到以上每一个小需求的demo,没找到的可以去找一找。

理想数据结构

  • 通过表格可知
    • 日期维度 为表格展示的最小分组
    • 每个日期下包含四种类型的数据
  • 结论
    • 一个日期可以拆分为 4个row
    • row的数据应为 日期、父级类型、类型、用户1、 用户 2…

模拟此数据结构

const data = [
  {
    date: "2022-07-01",
    parentType: "conversion_monitor", // 转换监控
    type: "conversion_7", // 7日转化率
    // 以下数据结构后面会有变动
    user1: "1%", //用户1的7日转化率
    user2: "3%", //用户2的7日转化率
    user3: "10%", //用户3的7日转化率
    user4: "0%", //用户4的7日转化率
  },
  {
    date: "2022-07-01",
    parentType: "conversion_monitor", // 转换监控
    type: "conversion_30", // 30日转化率
    // 以下数据结构后面会有变动
    user1: "2%", //用户1的30日转化率
    user2: "1%", //用户2的30日转化率
    user3: "5%", //用户3的30日转化率
    user4: "5%", //用户4的30日转化率
  },
  {
    date: "2022-07-01",
    parentType: "trend_monitor", // 趋势监控
    type: "short_trend", // 短期趋势
    // 以下数据结构后面会有变动
    user1: "-0.001", //用户1的7日转化率
    user2: "0.05", //用户2的7日转化率
    user3: "0.02", //用户3的7日转化率
    user4: "-0.04", //用户4的7日转化率
  },
  {
    date: "2022-07-01",
    parentType: "trend_monitor", // 趋势监控
    type: "long_trend", // 长期趋势
    // 以下数据结构后面会有变动
    user1: "-0.002", //用户1的7日转化率
    user2: "0.06", //用户2的7日转化率
    user3: "0.01", //用户3的7日转化率
    user4: "-0.03", //用户4的7日转化率
  },
];

实现Demo代码

基本表格展示
import { Table } from "antd";

const Demo1 = () => {
  const data = [
    {
      date: "2022-07-01",
      parentType: "conversion_monitor", // 转换监控
      type: "conversion_7", // 7日转化率
      // 以下数据结构后面会有变动
      user1: "1%", //用户1的7日转化率
      user2: "3%", //用户2的7日转化率
      user3: "10%", //用户3的7日转化率
      user4: "0%", //用户4的7日转化率
    },
    {
      date: "2022-07-01",
      parentType: "conversion_monitor", // 转换监控
      type: "conversion_30", // 30日转化率
      // 以下数据结构后面会有变动
      user1: "2%", //用户1的30日转化率
      user2: "1%", //用户2的30日转化率
      user3: "5%", //用户3的30日转化率
      user4: "5%", //用户4的30日转化率
    },
    {
      date: "2022-07-01",
      parentType: "trend_monitor", // 趋势监控
      type: "short_trend", // 短期趋势
      // 以下数据结构后面会有变动
      user1: "-0.001", //用户1的7日转化率
      user2: "0.05", //用户2的7日转化率
      user3: "0.02", //用户3的7日转化率
      user4: "-0.04", //用户4的7日转化率
    },
    {
      date: "2022-07-01",
      parentType: "trend_monitor", // 趋势监控
      type: "long_trend", // 长期趋势
      // 以下数据结构后面会有变动
      user1: "-0.002", //用户1的7日转化率
      user2: "0.06", //用户2的7日转化率
      user3: "0.01", //用户3的7日转化率
      user4: "-0.03", //用户4的7日转化率
    },
  ];
  const columns = [
    {
      title: "日期",
      dataIndex: "date",
    },
    {
      title: "监控指标",
      dataIndex: "parentType",
    },
    {
      title: "监控指标",
      dataIndex: "type",
    },
    {
      title: "user1",
      dataIndex: "user1",
    },
    {
      title: "user2",
      dataIndex: "user2",
    },
    {
      title: "user3",
      dataIndex: "user3",
    },
    {
      title: "user4",
      dataIndex: "user4",
    },
  ];
  return (
    <div>
      <h2>Demo1</h2>
      <Table
        columns={columns}
        dataSource={data}
        pagination={false}
        bordered
      ></Table>
    </div>
  );
};

export default Demo1;

在这里插入图片描述

【Demo1-codesandbox】

表头合并

通过 columns.colSpan 进行表头合并

  const columns = [
    {
      title: "日期",
      dataIndex: "date",
    },
    {
      title: "监控指标",
      colSpan: 2,  // 设置占位2格
      dataIndex: "parentType",
    },
    {
      title: "监控指标",
      colSpan: 0,  // 设置占位0格
      dataIndex: "type",
    },
    {
      title: "user1",
      dataIndex: "user1",
    },
    {
      title: "user2",
      dataIndex: "user2",
    },
    {
      title: "user3",
      dataIndex: "user3",
    },
    {
      title: "user4",
      dataIndex: "user4",
    },
  ];

在这里插入图片描述

【Demo2-codesandbox】

行数据合并

通过 columns.onCell 处理行合并

  const columns = [
    {
      title: "日期",
      dataIndex: "date",
      /**
       * 每4行都是一个日期,从index = 0 开始,
       * 每隔4行的rowSpan为4,其余的rowSpan为0
       */
      onCell: (_, index) => ({
        rowSpan: index % 4 === 0 || index === 0 ? 4 : 0,
      }),
    },
    {
      title: "监控指标",
      colSpan: 2,
      dataIndex: "parentType",
      /**
       * 每2行组成了一个维度的监控指标,从index = 0 开始,
       * 每隔2行的rowSpan为2,其余的rowSpan为0
       */
      onCell: (_, index) => ({
        rowSpan: index % 2 === 0 || index === 0 ? 2 : 0,
      }),
    },
    {
      title: "监控指标",
      colSpan: 0,
      dataIndex: "type",
    },
    {
      title: "user1",
      dataIndex: "user1",
    },
    {
      title: "user2",
      dataIndex: "user2",
    },
    {
      title: "user3",
      dataIndex: "user3",
    },
    {
      title: "user4",
      dataIndex: "user4",
    },
  ];

在这里插入图片描述

【Demo3-codesandbox】

数据处理(整合接口返回的数据结构)

:::info
其实上部分代码已经实现了产品的基本需求,但后台不一定能给我理想的数据结构,所以需要进行数据处理
:::

假设后台返回的数据结构如下:

  • 用户数量为动态
  • 返回日期为动态
[
  {
    "name": "傅洋",
    "id": "user_0",
    "dateList": [
      {
        "date": "2022-07-01",
        "conversion_7": "55%",
        "conversion_30": "64%",
        "short_trend": "-1.975",
        "long_trend": "0.053"
      },
      {
        "date": "2022-07-02",
        "conversion_7": "82%",
        "conversion_30": "27%",
        "short_trend": "0.779",
        "long_trend": "0.353"
      }
    ]
  },
  {
    "name": "白秀英",
    "id": "user_1",
    "dateList": [
      {
        "date": "2022-07-01",
        "conversion_7": "90%",
        "conversion_30": "32%",
        "short_trend": "0.462",
        "long_trend": "-1.763"
      },
      {
        "date": "2022-07-02",
        "conversion_7": "33%",
        "conversion_30": "52%",
        "short_trend": "-1.653",
        "long_trend": "0.725"
      }
    ]
  }
]

理想数据结构变更

因为数据返回的用户/时间数量为动态,所以 定义一个对象将 用户ID 作为 key, 值作为val 。这样更灵活一些

const data = [
  {
    date: "2022-07-01",
    parentType: "conversion_monitor", // 转换监控
    type: "conversion_7", // 7日转化率
    data:{
      user1: "1%",
      user2: "3%",
      user3: "10%",
      user4: "0%",
    }
  },
  {
    date: "2022-07-01",
    parentType: "conversion_monitor", // 转换监控
    type: "conversion_30", // 30日转化率
    data:{
      user1: "1%",
      user2: "3%",
      user3: "10%",
      user4: "0%",
    }
  },
  {
    date: "2022-07-01",
    parentType: "trend_monitor", // 趋势监控
    type: "short_trend", // 短期趋势
    data:{
      user1: "-0.001",
      user2: "0.05",
      user3: "0.02",
      user4: "-0.04",
    }
  },
  {
    date: "2022-07-01",
    parentType: "trend_monitor", // 趋势监控
    type: "long_trend", // 长期趋势
    data:{
      user1: "-0.001",
      user2: "0.05",
      user3: "0.02",
      user4: "-0.04",
    }
  },
];

mock数据

使用mockjs 生成测试数据

import { Random } from "mockjs";
/**
 * 生成模拟数据
 * @param {*} personNumber  人员数量
 * @param {*} date         日期数量
 * @returns
 */
const mockData = (personNumber = 10, date = 10) =>
  new Array(personNumber).fill("").map((_, index) => ({
    name: Random.cname(),
    id: `user_${index}`,
    dateList: new Array(date).fill("").map((_, index) => ({
      date: `2022-07-${index < 9 ? `0${index + 1}` : index + 1}`,
      conversion_7: `${Random.natural(0, 100)}%`,
      conversion_30: `${Random.natural(0, 100)}%`,
      short_trend: `${Random.float(-1, 1, 3, 3)}`,
      long_trend: `${Random.float(-1, 1, 3, 3)}`,
    })),
  }));

动态获取colums

因为用户数量不定,所以动态生成 colums

const getColumns = (data) =>
    data.map((item) => ({
      title: item.name,
      dataIndex: `data.${item.name}`,
    }));

const columns = [
  {
    title: "日期",
    dataIndex: "date",
    /**
       * 每4行都是一个日期,从index = 0 开始,
       * 每隔4行的rowSpan为4,其余的rowSpan为0
       */
    onCell: (_, index) => ({
      rowSpan: index % 4 === 0 || index === 0 ? 4 : 0,
    }),
  },
  {
    title: "监控指标",
    colSpan: 2,
    dataIndex: "parentType",
    /**
       * 每2行组成了一个维度的监控指标,从index = 0 开始,
       * 每隔2行的rowSpan为2,其余的rowSpan为0
       */
    onCell: (_, index) => ({
      rowSpan: index % 2 === 0 || index === 0 ? 2 : 0,
    }),
  },
  {
    title: "监控指标",
    colSpan: 0,
    dataIndex: "type",
  },
  ...getColumns(response),
];

数据转换逻辑

  • 获取所有的日期数组
  • 日期从大到小排序
  • 通过遍历的当前日期 下 所有人员的 统计数据。每个日期生成四条记录

数据转换方法(for)

/**
 * 数据转换方法
 * @param {*} response 接口返回的数据
 */
const dataProcessing = (response) => {
  // 获取所有的日期KEy
  let dateList = [];
  // 遍历人员的所有数据
  for (let i = 0; i < response.length; i++) {
    const item = response[i];
    // 遍历人员的所有日期
    for (let j = 0; j < item.dateList.length; j++) {
      const dateItem = item.dateList[j];
      // 如果日期不存在,则添加到日期列表中
      if (!dateList.includes(dateItem.date)) {
        // 日期添加到日期列表中
        dateList.push(dateItem.date);
      }
    }
  }
  // 根据日期排序
  dateList = dateList.sort((a, b) => {
    return new Date(b).getTime() - new Date(a).getTime();
  });

  // 根据日期分组
  let data = [];
  // 遍历日期
  for (let i = 0; i < dateList.length; i++) {
    // 获取日期
    const date = dateList[i];
    // 每一个日期需要生成4个维度的监控指标
    const arr = [
      //七日转化率
      {
        date: date,
        parentType: "conversion_monitor",
        type: "conversion_7",
        data: {},
      },
      //三十日转化率
      {
        date: date,
        parentType: "conversion_monitor",
        type: "conversion_30",
        data: {},
      },
      //短期趋势
      {
        date: date,
        parentType: "trend_monitor",
        type: "short_trend",
        data: {},
      },
      //长期趋势
      {
        date: date,
        parentType: "trend_monitor",
        type: "long_trend",
        data: {},
      },
    ];
    // 遍历人员
    for (let j = 0; j < response.length; j++) {
      const item = response[j];
      // 获取当前日期的的用户数据
      const dateItemData = item.dateList.find((item) => item.date === date);
      if (dateItemData) {
        arr[0].data[item.id] = dateItemData.conversion_7;
        arr[1].data[item.id] = dateItemData.conversion_30;
        arr[2].data[item.id] = dateItemData.short_trend;
        arr[3].data[item.id] = dateItemData.long_trend;
      }
    }
    data.push(...arr);
  }
  return data;
};

【Demo4-codesandbox】

数据转换方法(es6)

/**
 * 数据转换方法
 * @param {*} response 接口返回的数据
 */
const dataProcessing = (response) =>
  response
    // 获取到出现的所有日期
    .reduce((acc, item) => {
      return acc.concat(
        item.dateList
          .map((item) => item.date)
          .filter((item) => !acc.includes(item))
      );
    }, [])
    // 日期排序 从大到小
    .sort((a, b) => new Date(b).getTime() - new Date(a).getTime())
    // 根据日期顺序生成日期列表
    .reduce((list, date) => {
      // 获取当前日期的的用户数据
      const dataSource = response.map((item) => ({
        // 将用户ID 回填到数据中
        id: item.id,
        ...(item.dateList.find((item) => item.date === date) || {}),
      }));
      // 每一个日期需要生成4个维度的监控指标
      return list.concat([
        //七日转化率
        {
          date: date,
          parentType: "conversion_monitor",
          type: "conversion_7",
          data: dataSource.reduce((data, item) => {
            data[item.id] = item.conversion_7;
            return data;
          }, {}),
        },
        //三十日转化率
        {
          date: date,
          parentType: "conversion_monitor",
          type: "conversion_30",
          data: dataSource.reduce((data, item) => {
            data[item.id] = item.conversion_30;
            return data;
          }, {}),
        },
        //短期趋势
        {
          date: date,
          parentType: "trend_monitor",
          type: "short_trend",
          data: dataSource.reduce((data, item) => {
            data[item.id] = item.short_trend;
            return data;
          }, {}),
        },
        //长期趋势
        {
          date: date,
          parentType: "trend_monitor",
          type: "long_trend",
          data: dataSource.reduce((data, item) => {
            data[item.id] = item.long_trend;
            return data;
          }, {}),
        },
      ]);
    }, []);

【Demo5-codesandbox】

最终输出

最终版本使用的是es6版本的代码,这种模式也是我常用的。至于原因就是我可以少敲两行代码

  • 增加左侧固定列
  • 增加X轴及Y轴滚动
  • 增加字典,将key转中文(ps:无il18n需求)
import { Table } from "antd";
import { Random } from "mockjs";
/**
* 生成模拟数据
* @param {*} personNumber  人员数量
* @param {*} date         日期数量
* @returns
*/
const mockData = (personNumber = 30, date = 10) =>
new Array(personNumber).fill("").map((_, index) => ({
  name: Random.cname(),
  id: `user_${index}`,
  dateList: new Array(date).fill("").map((_, index) => ({
    date: `2022-07-${index < 9 ? `0${index + 1}` : index + 1}`,
    conversion_7: `${Random.natural(0, 100)}%`,
    conversion_30: `${Random.natural(0, 100)}%`,
    short_trend: `${Random.float(-1, 1, 3, 3)}`,
    long_trend: `${Random.float(-1, 1, 3, 3)}`,
  })),
}));

/**
* 获取Columns
* @param {*} data
*/
const getColumns = (data) =>
data.map((item) => ({
  title: item.name,
  width: 80,
  dataIndex: ["data", item.id],
}));

/**
* 数据转换方法
* @param {*} response 接口返回的数据
*/
const dataProcessing = (response) =>
response
// 获取到出现的所有日期
.reduce((acc, item) => {
  return acc.concat(
    item.dateList
    .map((item) => item.date)
    .filter((item) => !acc.includes(item))
  );
}, [])
// 日期排序 从大到小
.sort((a, b) => new Date(b).getTime() - new Date(a).getTime())
// 根据日期顺序生成日期列表
.reduce((list, date) => {
  // 获取当前日期的的用户数据
  const dataSource = response.map((item) => ({
    // 将用户ID 回填到数据中
    id: item.id,
    ...(item.dateList.find((item) => item.date === date) || {}),
  }));
  // 每一个日期需要生成4个维度的监控指标
  return list.concat([
    //七日转化率
    {
      date: date,
      parentType: "conversion_monitor",
      type: "conversion_7",
      data: dataSource.reduce((data, item) => {
        data[item.id] = item.conversion_7;
        return data;
      }, {}),
    },
    //三十日转化率
    {
      date: date,
      parentType: "conversion_monitor",
      type: "conversion_30",
      data: dataSource.reduce((data, item) => {
        data[item.id] = item.conversion_30;
        return data;
      }, {}),
    },
    //短期趋势
    {
      date: date,
      parentType: "trend_monitor",
      type: "short_trend",
      data: dataSource.reduce((data, item) => {
        data[item.id] = item.short_trend;
        return data;
      }, {}),
    },
    //长期趋势
    {
      date: date,
      parentType: "trend_monitor",
      type: "long_trend",
      data: dataSource.reduce((data, item) => {
        data[item.id] = item.long_trend;
        return data;
      }, {}),
    },
  ]);
}, []);

/** 字典map */
const dictMap = {
  conversion_monitor: "转化监控",
  trend_monitor: "趋势监控",
  conversion_7: "7天转化率",
  conversion_30: "30天转化率",
  short_trend: "短期趋势",
  long_trend: "长期趋势",
};

const Demo5 = () => {
  // 生成 mock 数据
  const response = mockData();
  // 获取所有的日期KEy
  const dateList = dataProcessing(response);
  
  const columns = [
    {
      title: "日期",
      dataIndex: "date",
      width: 100,
      fixed: "left",
      /**
      * 每4行都是一个日期,从index = 0 开始,
      * 每隔4行的rowSpan为4,其余的rowSpan为0
      */
      onCell: (_, index) => ({
        rowSpan: index % 4 === 0 || index === 0 ? 4 : 0,
      }),
    },
    {
      title: "监控指标",
      colSpan: 2,
      fixed: "left",
      width: 80,
      dataIndex: "parentType",
      /**
      * 每2行组成了一个维度的监控指标,从index = 0 开始,
      * 每隔2行的rowSpan为2,其余的rowSpan为0
      */
      onCell: (_, index) => ({
        rowSpan: index % 2 === 0 || index === 0 ? 2 : 0,
      }),
      render: (text) => dictMap[text],
    },
    {
      title: "监控指标",
      colSpan: 0,
      fixed: "left",
      width: 100,
      dataIndex: "type",
      render: (text) => dictMap[text],
    },
    ...getColumns(response),
  ];
  return (
    <div>
      <h2>最终版本</h2>
      <Table
        columns={columns}
        dataSource={dateList}
        pagination={false}
        bordered
        size="small"
        scroll={{ x: 1, y: 600 }}
        ></Table>
    </div>
  );
};

export default Demo5;

【Demo6-codesandbox】

  • 8
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

赵忠洋

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值