JS数组处理:提取嵌套属性到根层级 重组工时统计数据结构
动态月份工时表格数据结构调整方案
问题背景
我基于此前的问题 React displaying data in a table with dynamic columns 开展开发。
此前已实现用户选择月份后,表格按日展示对应客户工时的功能;现在需要扩展该模块:管理员选择用户、月份、年份后,表格表头自动填充所选月份的全部日期,每一行对应一个客户的工时数据。经过多步数据处理,得到的结果距离预期结构仅差一步,目前遇到阻塞。
现有代码与数据集
以下是简化后的数据集和当前实现逻辑:
const data = [ { start: "2022-04-3", hours: 1, client: { id: "123", fullName: "Tom Tom" } }, { start: "2022-04-10", hours: 2, client: { id: "123", fullName: "Tom Tom" } }, { start: "2022-04-10", hours: 3, client: { id: "123", fullName: "Tom Tom" } }, { start: "2022-04-10", hours: 1, client: { id: "123", fullName: "Tom Tom" } }, { start: "2022-04-13", hours: 1, client: { id: "456", fullName: "Jack Jack" } }, { start: "2022-04-13", hours: 2, client: { id: "123", fullName: "Tom Tom" } }, { start: "2022-04-15", hours: 1, client: { id: "789", fullName: "Pete Pete" } }, { start: "2022-04-15", hours: 1, client: { id: "789", fullName: "Pete Pete" } } ] let collData = data.reduce((accumulator, currentValue) => { let existing = accumulator.find((n) => n.client.id === currentValue.client.id && n.start === currentValue.start); if (existing) { existing.hours += currentValue.hours; } else { accumulator.push(currentValue); } return accumulator; }, []); console.log("step 1 ", collData); let res = {}; collData.forEach((item) => { const splitDate = item.start.split("-"); const day = splitDate[splitDate.length - 1]; const newStat = { start: parseInt(day), hours: item.hours}; if(res[item.client.id]) { let stats = res[item.client.id].stats; stats = [...stats, newStat]; res[item.client.id].stats = stats; } else{ res[item.client.id] = { client: item.client, stats: [newStat] } } }); let filtArr = []; const daysInMonth = 30; let dayInfo = []; for (let i = 1; i <= daysInMonth; i++) { dayInfo.push(i) // create an array and fill hours with 0 var dayArr = dayInfo.map((day) => ({ day: day, hours: 0 })); } const displayDates = (id) => { const client = res[id]; let temp = []; dayArr.map((i, idx) => { const getHours = client.stats.find((item) => item.start === i.day); let newStats = {}; if (getHours) { newStats = { client: {fullName: client.client.fullName, id: client.client.id}, stats: [{ day: i.day, hours: getHours.hours, }] }; } else{ newStats = { client: {fullName: client.client.fullName, id: client.client.id}, stats: [{ day: i.day, hours: 0, }] }; } temp[idx] = newStats; }); return temp; } Object.keys(res).forEach(id=>{ filtArr = [...filtArr, res[id]]; console.log(displayDates(id)) })
预期结构说明
为了实现表格行和数据的映射渲染,需要将最终数据调整为指定结构,当前输出中每个日期都会重复生成携带client信息的对象:
当前错误输出
current output = [{ client: { fullName: "Pete Pete", id: "789" }, stats: [{ day: 1, hours: 0 }] }, { client: { fullName: "Pete Pete", id: "789" }, stats: [{ day: 2, hours: 0 }] }, { client: { fullName: "Pete Pete", id: "789" }, stats: [{ day: 3, hours: 0 }] },//and so on for all days of the month ]
理想输出结构
ideal output= [ { client: {name: "name name", id: "123"}, stats: [ {day: 1, hours: 0}, {day: 2, hours: 1}, {day: 3, hours: 0}, {day: 4, hours: 2}, //and so on for all the days in the month ] } ]
此前尝试使用map、forEach方法调整数据结构,但始终无法正确访问数组内对象属性,频繁出现undefined报错。猜测可以在现有处理逻辑的某一步直接生成目标结构,但受限于个人能力未能实现。在线运行示例:https://jsfiddle.net/joznox/xzd5y142/3/
修复方案
原代码有两个核心问题:
- 生成
dayArr的逻辑错误,map操作写在了for循环内部,每次循环都会覆盖dayArr,最终只能得到长度为1的数组 displayDates函数遍历每一天时都新建了携带client信息的独立对象,导致单个客户的数据被拆成了30条
直接替换原有数据处理逻辑即可得到目标结构:
// 第一步:合并同客户同日期的重复工时记录 const collData = data.reduce((accumulator, currentValue) => { const existing = accumulator.find((n) => n.client.id === currentValue.client.id && n.start === currentValue.start); if (existing) { existing.hours += currentValue.hours; } else { // 深拷贝避免修改原始数据 accumulator.push(structuredClone(currentValue)); } return accumulator; }, []); // 目标月份总天数,生产环境可通过new Date(选中年份, 选中月份, 0).getDate()动态计算 const daysInMonth = 30; // 预生成整月默认工时为0的统计数组 const defaultMonthStats = Array.from({length: daysInMonth}, (_, idx) => ({ day: idx + 1, hours: 0 })); const clientMap = {}; collData.forEach(item => { const clientId = item.client.id; // 解析工时记录对应的日期(天) const recordDay = Number(item.start.split('-').at(-1)); // 客户不存在时初始化数据,直接填充整月默认0工时 if (!clientMap[clientId]) { clientMap[clientId] = { client: { id: clientId, name: item.client.fullName }, stats: structuredClone(defaultMonthStats) } } // 找到对应日期的索引,更新实际工时 const targetIndex = clientMap[clientId].stats.findIndex(stat => stat.day === recordDay); if (targetIndex !== -1) { clientMap[clientId].stats[targetIndex].hours = item.hours; } }); // 最终输出符合要求的结构 const finalResult = Object.values(clientMap); console.log(finalResult);
逻辑说明
- 提前生成整月默认0工时的统计数组,不需要后续反复判断日期是否存在
- 用对象做客户ID到客户数据的映射,查找效率远高于数组遍历find
- 初始化客户数据时直接挂载整月默认统计,后续只需要替换有工时记录的日期对应的hours值即可
- 最终直接取映射对象的value集合,每个客户对应一条数据,stats数组按顺序包含整月所有日期的工时,可直接用于表格渲染
内容的提问来源于stack exchange,提问作者Jowz
相关产品推荐
相关产品推荐

