很多小伙伴对于产品提出的陌生需求就“麻爪”。其实如何“解题”很重要,只要掌握正确的思考方式真的很简单。
效果展示
GIF 转换完比较糊
环境
antd@2.x 部分写法与本文不一样,但是思路是一样的
包名 | 版本 |
---|---|
react | ^16.14.0 |
antd | 4.22 |
分析需求
- 涉及 列左侧固定
- 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;
表头合并
通过 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",
},
];
行数据合并
通过 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",
},
];
数据处理(整合接口返回的数据结构)
:::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;
};
数据转换方法(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;
}, {}),
},
]);
}, []);
最终输出
最终版本使用的是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;