|
|
|
|
公众号矩阵

使用Antd表格组件实现日程表

20多天前,遇到一个日程表的业务需求,可以动态增加列、对单元格进行合并,结合公司的jsp项目的已有功能完成单元格的增、删、改操作。

作者:神奇的程序员K来源:神奇的程序员k|2020-11-20 10:52

本文转载自微信公众号「神奇的程序员K」,作者神奇的程序员K 。转载本文请联系神奇的程序员K公众号。

 前言

20多天前,遇到一个日程表的业务需求,可以动态增加列、对单元格进行合并,结合公司的jsp项目的已有功能完成单元格的增、删、改操作。进行需求分析整理后,经过了一番查找,发现React版本的antd的表格组件功能很强大,可定制程度很高,可以助我完成这个业务需求的开发。

由于要和jsp进行交互,所以在实现过程中,遇到了一些难题踩了挺多坑,本文就跟大家分享下我从0到1实现这个需求的过程与思路,欢迎各位感兴趣的开发者阅读本文。

环境搭建

因为公司的项目是基于jsp的,antd本想用Vue版本的,无奈它与jsp的一些语法冲突了跑不起来,于是就尝试了react版本的antd,它跑起来了没有发现任何兼容性问题,一切正常。给React点个赞??。

由于要与项目中已有的功能进行交互,没法用脚手架,我只能以cdn的方式引入react,如下所示,按顺序引入react、axios、lodah以及antd所需要的文件。

  1. <script crossOrigin type="text/javascript" src="lib/react.production.min.js"></script> 
  2.    <script crossOrigin type="text/javascript" src="lib/react-dom.production.min.js"></script> 
  3.    <script src="lib/babel.min.js"></script> 
  4.    <script type="text/javascript" src="lib/moment.min.js"></script> 
  5.    <script src="lib/lodash.min.js"></script> 
  6.    <script type="text/javascript" src="lib/antd.min.js"></script> 
  7.    <script type="text/javascript" src="lib/axios.min.js"></script> 
  8.    <link rel="stylesheet" href="lib/antd.min.css"

上述用到的资源文件地址: react-antd-schedule/lib

我们需要把react相关代码写在text/babel标签中,如下所示,我们打印antd和react看看是否有值。

  1. <script type="text/babel"
  2.     console.log("react"); 
  3.     console.log(React); 
  4.     console.log("antd"
  5.     console.log(antd); 
  6. </script> 

打开浏览器控制台,出现下述信息,代表我们的环境已经搭建成功。

image-20201119155715157

接下来,我们写个HelloWord来测试下效果。

  1. <div id="root" style="width: 94%;overflow: hidden"></div> 
  2. <script type="text/babel"
  3.     // 自定义hook 
  4.     const App = () => { 
  5.         const onChange = (date, dateString) => { 
  6.             console.log(date, dateString); 
  7.         } 
  8.         return ( 
  9.             <div> 
  10.                 React+antd引入成功 
  11.                 <br /> 
  12.                 <antd.DatePicker onChange={onChange} /> 
  13.             </div> 
  14.         ); 
  15.     }; 
  16.     ReactDOM.render(<App />, document.getElementById("root")); 
  17. </script> 
 

执行上述代码,打开浏览器如果看到下述效果,就证明我们的环境已经搭好了。

image-20201119161505912

需要注意的是,CDN引入React和antd,他们是在全局暴露了一个对象,在使用它内部的方法时就需要React.xx、antd.xx来访问了。

需求分析

当我收到需求简述后,我对其进行了整理:

  • 表格列要展示的内容:日期、日程内容(接口动态返回),日程内容列用户可以自己手动增加。
  • 表格行展示的内容为每一天的数据,每一天的数据分为:上午、下午、晚上三个时间段。
  • 日程内容分为天日程和某个时间段的日程两种状态,如果为天日程则需要进行单元格合并。
  • 日程内容列的每个单元格有5种状态,需要通过某种方式来区分,让用户一眼就能看出当前日程处于什么状态。
  • 日程内容单元格的内容如果为空时,需要将单元格进行合并,显示一个增加图标,点击增加图标后,打开系统的弹窗进行增加操作,操作完成后,渲染内容至刚才点击的单元格。
  • 如果内容单元格有内容时,根据不同的状态,打开不同的弹窗进行改、删操作,操作完后,更新结果至对应的单元格。

需求确定后,老板给我分了一个后端,跟后端沟通后开发周期估了1周,我页面估了2天的时间,剩下的3天与后端进行数据对接。

2天后,我把页面弄完了,表格需要的数据格式也定义好了,把数据格式发给后端后,他说好,没问题。

因为没有UI给设计图,所以第一版,我就凭着自己的直觉来弄了,搞出来的东西蛮丑的,下图就是我根据需求实现的页面。

image-20201119172808318

然而,事情没有预想中那么顺利,我页面做好后,到开发周期的最后一天下午,后端把接口给我了,但返回的数据不是我预想的格式,我又进行了二次处理,页面渲染出来后,快到下班时间了,到了预估的开发时间没有完成需求,倒也能理解,毕竟后端那边要处理的数据比较复杂。

本来预估了一周的开发时间,后面需求的不断增加、变更、UI设计效果图,我的页面代码也从一开始的100多行累加到现在的1000多行,这一套折腾下来,直到需求开发完成交给测试,花了20多天的时间。

需求实现

接下来,就跟大家分享下在实现这个需求时,遇到的难点、踩到的一些坑以及我的解决方案。

最后实现的效果如下所示,实现代码请移步:react-antd-schedule/index.html

image-20201119175256753

动态增加列

这个日程表用户可以通过点增加图标来增加一列日程,此时我们就需要往表格头部增加一列数据,一开始我觉得只要往antd的columns和dataSource中添加一条数据就行了,如下所示:

  1. const App = () => { 
  2.         const [columns, setColumns] = React.useState([]); 
  3.         const [optRecords, setOptRecords] = React.useState([]); 
  4.            //增加按钮函数 
  5.         const btnClick = (e) => { 
  6.             index++; 
  7.             let columnsObj = { 
  8.                 dataIndex: 'rcnr' + (index), 
  9.                 title: '日程内容' + index
  10.                 align: 'center'
  11.                 onCell: tdSet, 
  12.                 render: rctd_render, 
  13.             } 
  14.             // 表格列新增一列 
  15.             columns.push(columnsObj) 
  16.             setColumns(columns); 
  17.             // 处理表格数据 
  18.             for (let i = 0; i < optRecords.length; i++) { 
  19.                 let key = "rcnr"+index
  20.                 // 表格数据新增一条 
  21.                 optRecords[i][key] = {text:"", code:"0"
  22.             } 
  23.             setOptRecords(optRecords); 
  24.         } 
  25.  } 

当我在浏览器执行看效果时,发现没有生效,于是我下意识的打开了浏览器控制台看看是不是报错了,啪的一下,很快啊~新增加的那一列被渲染上去了,我大E了啊,antd不讲武德啊。

于是,我多试了几次,发现还是不渲染,打开控制台后就奇迹般的渲染上去了,有点摸不着头脑,就求助了下网友,我才恍然大悟,原来是antd没有监听到引用地址的改变,得到了下述解决方案,用一个函数去处理它,让antd监听到引用地址改变,它才会将数据进行渲染。

  1. const App = () => { 
  2.        const [optRecords, setOptRecords] = React.useState([]); 
  3.        const [columns, setColumns] = React.useState([]); 
  4.           //增加按钮函数 
  5.        const btnClick = (e) => { 
  6.            if (tableLoadingStatus) { 
  7.                alert("表格数据尚未加载完成"); 
  8.                return false
  9.            } 
  10.            columnsIndex++; 
  11.            let columnsObj = { 
  12.                dataIndex: "rcnr" + (columnsIndex), 
  13.                title: "日程内容" + columnsIndex, 
  14.                align: "left"
  15.                className: "rcnrfontSet"
  16.                width: 189.5, 
  17.                onCell: tdSet, 
  18.                render: rctd_render 
  19.            }; 
  20.            // 表格列新增一列 
  21.            setColumns((arr => [...arr, columnsObj])); 
  22.            // 处理表格数据 
  23.            setOptRecords((arr) => arr.map((item) => { 
  24.                return { ...item, ["rcnr" + columnsIndex]: { wz: columnsIndex - 1 } }; 
  25.            })); 
  26.             
  27.        }; 

表格列补齐

在后端返回的数据中,如果有不存在的日程,直接连字段都没返回,这就造成了antd在渲染的时候列与表格数据不对应而引发的武发渲染的问题,于是我只能把所有数据遍历一遍,求出最大列长度,然后将列少的数据进行补全,由于添加数据时接口需要传当前点击的是哪一列,刚才补全的数据中是不包含wz字段的,因此我们需要再遍历一次数据,把wz字段加上去,代码如下:

  1. // 表格数据渲染函数 
  2.         const tableDataRendering = function(res) { 
  3.           // 获取最大子节点的key数量 
  4.             let maxChildLength = Object.keys(defaultData[0].children[0]).length; 
  5.             for (let i = 0; i < defaultData.length; i++) { 
  6.                 for (let j = 0; j < defaultData[i].children.length; j++) { 
  7.                     const currentObjLength = Object.keys(defaultData[i].children[j]).length; 
  8.                     if (currentObjLength > maxChildLength) { 
  9.                         maxChildLength = currentObjLength; 
  10.                     } 
  11.                 } 
  12.             } 
  13.  
  14.             // 补齐缺少的节点 
  15.             for (let i = 0; i < defaultData.length; i++) { 
  16.                 for (let j = 0; j < defaultData[i].children.length; j++) { 
  17.                     const currentObjLength = Object.keys(defaultData[i].children[j]).length; 
  18.                     // 当前节点的长度小于第一个子节点的长度就补齐 
  19.                     for (let k = currentObjLength; k < maxChildLength; k++) { 
  20.                         defaultData[i].children[j]["rcnr" + k] = {}; 
  21.                     } 
  22.                 } 
  23.             } 
  24.  
  25.             // 如果存在空对象添加位置字段 
  26.             for (let i = 0; i < defaultData.length; i++) { 
  27.                 for (let j = 0; j < defaultData[i].children.length; j++) { 
  28.                     // 获取每天的时间段对象 
  29.                     const item = defaultData[i].children[j]; 
  30.                     // 获取所有的key 
  31.                     const keys = Object.keys(item); 
  32.                     // 提取所有的日程字段 
  33.                     for (let k = 1; k < keys.length; k++) { 
  34.                         // 日程为空添加wz字段 
  35.                         if (Object.keys(item[keys[k]]).length <= 1) { 
  36.                             defaultData[i].children[j][keys[k]].wz = k - 1; 
  37.                         } 
  38.                     } 
  39.                 } 
  40.             } 
  41.         } 

监听子窗口关闭

但点击单元格做完对应的操作后,弹窗关闭,此时我们需要在当前页面监听到子窗口关闭,然后向后台请求接口重新获取数据渲染页面,在打开的弹窗中提供了一个方法,可以调用父页面的方法,但是这个方法必须写在hooks外面他才能获取到。

此时,问题就产生了,如果写在hooks外面,那么就无法拿到antd表格内部的数据做到页面重新渲染,经过一番思考后,想到了可以Proxy来实现,当被代理的对象发生改变时,就触发hooks里的代理函数,实现代码如下:

  1. <script type="text/babel"
  2.       // 声明代理变量 
  3.     let pageStateEngineer; 
  4.     // 需要进行代理的对象 
  5.     let pageState = { status: false }; 
  6.     // 监听子页面关闭,弹窗页面在关闭时可调用这个方法,触发页面刷新 
  7.     const getSubpageData = (status) => { 
  8.         console.log("子页面关闭"); 
  9.         pageStateEngineer.status = true
  10.     }; 
  11.     const App = () => { 
  12.         // 代理处理函数 
  13.         const pageStateHandler = { 
  14.             setfunction(recObj, key, value) { 
  15.                 // 表格状态改为正在加载 
  16.                 setTableLoadingStatus(true); 
  17.                 // 重新请求接口,获取最新数据 
  18.                 axios.post('http://mock-api.com/mnE66LKJ.mock/getTableListData', { 
  19.                 }).then(function(res) { 
  20.                     // 数据请求成功,改变表格加载层状态 
  21.                     setTableLoadingStatus(false); 
  22.                     if (res.status === 200) { 
  23.                         // 执行表格数据渲染函数 
  24.                         tableDataRendering(res); 
  25.                     } else { 
  26.                         alert("服务器错误"); 
  27.                     } 
  28.                 }); 
  29.                 // 修改对象属性 
  30.                 recObj[key] = value; 
  31.                 return true
  32.             } 
  33.         }; 
  34.          
  35.         // 第一次渲染时,在借口调用成功后创建proxy 
  36.         React.useEffect(() => { 
  37.             // 调用接口获取表格数据 
  38.             axios.post('http://mock-api.com/mnE66LKJ.mock/getTableListData', { 
  39.                 ls: 0, 
  40.                 ts: 0 
  41.             }).then(function(res) { 
  42.                 //创建代理,监听pageState对象改变,pageStateHandler处理变更 
  43.                 pageStateEngineer = new Proxy(pageState, pageStateHandler); 
  44.             }) 
  45.         } 
  46.     } 
  47. </script> 

重新渲染表格

用户在使用日程表时,他会执行删除某个日程,此时表格渲染函数就要从columns和dataSource中各删除一条数据了,一开始我是直接覆盖其数据,这样做引用地址没变,就引发了动态增加列的那个bug,antd监听不到引用地址改变没有刷新页面。但是我又不知道用户具体删了哪条数据,不好自己写函数去处理。

经过一番求助后,得到了三个解决方案:

  • 使用immer来解决这个问题,经过折腾后还是没实现,他返回的数组是只读的,antd无法对数据进行操作,故放弃。
  • 使用use-immer来替代React的useState来解决这个问题,这个就比较坑爹了,官方提供了umd的js库,但是通过cdn引入进来后,我硬是没找到它暴露出来的对象是哪个,没法用,故放弃。
  • 使用lodash的cloneDeep方法进行深拷贝让其引用地址改变,这样antd就能监听到数据改变,从而触发页面刷新。

三个解决方案,经过验证后,只有第三个是可行的,于是我采取了它,实现代码如下:

  1. const App = () => { 
  2.         // 表格列格式定义 
  3.         const defaultColumns = [ 
  4.             { 
  5.                 dataIndex: "rq"
  6.                 title: "日期"
  7.                 align: "center"
  8.                 fixed: "left"
  9.                 colSpan: 2, 
  10.                 width: 140.5, 
  11.                 className: "rqfontSet"
  12.                 onCell: dateHandle, 
  13.                 render: (value, item, index) => {} 
  14.             }, 
  15.             { 
  16.                 dataIndex: "sjd"
  17.                 title: "时间段"
  18.                 width: 70, 
  19.                 colSpan: 0, 
  20.                 fixed: "left"
  21.                 align: "center"
  22.                 className: "sjdfontSet"
  23.                 render: (value, item, index) => { 
  24.                     let v1 = value.charAt(0); 
  25.                     let v2 = value.charAt(1); 
  26.                     return <div>{v1}<br />{v2}</div>; 
  27.                 } 
  28.             } 
  29.         ]; 
  30.  
  31.         // 表格数据渲染函数 
  32.         const tableDataRendering = function(res) { 
  33.           // 根据日程列字段数据赋值表格列的日程字段,rcList中包含sjd所以需要1开始 
  34.             for (let i = 1; i < rcList.length; i++) { 
  35.                 let rcnr = { 
  36.                     dataIndex: rcList[i], 
  37.                     title: "日程内容" + i, 
  38.                     align: "left"
  39.                     width: 189.5, 
  40.                     className: "rcnrfontSet"
  41.                     onCell: tdSet, 
  42.                     render: rctd_render 
  43.                 }; 
  44.                 defaultColumns.push(rcnr); 
  45.             } 
  46.  
  47.             // 渲染表格数据 
  48.             handleData(defaultData); 
  49.             // 渲染表格列,使用cloneDeep进行深拷贝,触发useState的更新 
  50.             setColumns(_.cloneDeep(defaultColumns)); 
  51.         } 
  52.      // 计算要合并的列数 
  53.         const handleData = (data) => { 
  54.             if (data == null) { 
  55.                 data = defaultData; 
  56.             } 
  57.             let newArr = []; 
  58.             data.map(item => { 
  59.                 if (item.children) { 
  60.                     item.children.forEach((subItem, i) => { 
  61.                         let obj = { ...item }; 
  62.                         Object.assign(obj, subItem); 
  63.                         delete obj.children; 
  64.                         obj.rowLength = item.children.length; 
  65.                         newArr.push(obj); 
  66.                     }); 
  67.                 } 
  68.             }); 
  69.             // console.log("处理好的表格数据"); 
  70.             // console.log(newArr); 
  71.             // 将处理好的数据放入optRecords,使用cloneDeep进行深拷贝,触发useState的更新 
  72.             setOptRecords(_.cloneDeep(newArr)); 
  73.         }; 
  74.   } 

还有一种解决方案是使用JSON.parse进行深拷贝,但是这种深拷贝有个问题:但json数据中有函数时,里面的函数会失效没法执行,由于我需要自定义antd的表格,在json数据中包含了函数,因此我不能使用这个方法。

触顶/触底加载数据

由于业务需要,不能使用antd的分页功能,需要实现触顶向前加载30条数据,触底向后加载30条数据。总共只能加载3个月的数据。

实现代码如下:

这里需要比较坑的地方就是如果触顶/触底时,拖动横向滚动也会触发滚动监听,因此我们需要排除横向滚动事件。

  1. <script type="text/babel"
  2.     // 触顶数据起始条数 
  3.     let dataToppingStartNum = 0; 
  4.     // 触底数据起始条数 
  5.     let dataBottomOutStartNum = 30; 
  6.     // 横向/垂直滚动条起始位置 
  7.     let levelPosition; 
  8.     let verticalPosition; 
  9.     // 触底/触顶次数 
  10.     let topFrequency = 0; 
  11.     let bottomFrequency = 0; 
  12.     const App = () => { 
  13.         // 横向滚动条位置 
  14.         levelPosition = document.querySelector(".ant-table-body").scrollLeft; 
  15.         // 纵向滚动条位置 
  16.         verticalPosition = document.querySelector(".ant-table-body").scrollTop; 
  17.         // 获取表格容器 
  18.         let antdTable = document.querySelector(".ant-table-body"); 
  19.         //页面滚动监听 
  20.         antdTable.onscroll = function() { 
  21.             // 触底向后加载数据 
  22.             if (antdTable.scrollTop + antdTable.clientHeight >= antdTable.scrollHeight) { 
  23.                 // 判断是否横向滚动 
  24.                 if (antdTable.scrollLeft !== levelPosition) { 
  25.                     // 更新位置 
  26.                     levelPosition = antdTable.scrollLeft; 
  27.                     return false
  28.                 } 
  29.                 // 第一次触底不触发数据加载 
  30.                 if (bottomFrequency === 0) { 
  31.                     bottomFrequency++; 
  32.                     return false
  33.                 } 
  34.                 if (bottomFrequency > 0) { 
  35.                     bottomFrequency = 0; 
  36.                 } 
  37.                 dataBottomOutStartNum += 30; 
  38.                 // 判断已加载的数据 
  39.                 if (dataBottomOutStartNum > 90) { 
  40.                     alert("最多只能向后加载90天的数据"); 
  41.                     return false
  42.                 } 
  43.                 // 保留向上滑动的天数 
  44.                 let bottomTS = 0; 
  45.                 // 页面第一次向上滑动,修改位置 
  46.                 if (dataToppingStartNum !== 0) { 
  47.                     bottomTS = -30; 
  48.                 } 
  49.                 setTableLoadingStatus(true); 
  50.                 axios.post('http://mock-api.com/mnE66LKJ.mock/getTableListData', { 
  51.                     ts: bottomTS, 
  52.                     ls: dataBottomOutStartNum 
  53.                 }).then(function(res) { 
  54.                     // 数据请求成功,改变表格加载层状态 
  55.                     setTableLoadingStatus(false); 
  56.                     if (res.status === 200) { 
  57.                         // 执行表格数据渲染函数 
  58.                         tableDataRendering(res); 
  59.                     } else { 
  60.                         alert("服务器错误"); 
  61.                     } 
  62.                 }); 
  63.             } 
  64.  
  65.             // 触顶向前加载数据 
  66.             if (antdTable.scrollTop === 0) { 
  67.                 // 判断是否横向滚动 
  68.                 if (antdTable.scrollLeft !== levelPosition) { 
  69.                     // 更新位置 
  70.                     levelPosition = antdTable.scrollLeft; 
  71.                     return false
  72.                 } 
  73.                 // 第一次触顶不触发数据加载 
  74.                 if (topFrequency === 0) { 
  75.                     topFrequency++; 
  76.                     return false
  77.                 } 
  78.                 if (topFrequency > 0) { 
  79.                     topFrequency = 0; 
  80.                 } 
  81.                 dataBottomOutStartNum += 30; 
  82.                 if (dataBottomOutStartNum > 90) { 
  83.                     alert("最多只能向前加载90天的数据"); 
  84.                     return false
  85.                 } 
  86.                 dataToppingStartNum -= 30; 
  87.                 setTableLoadingStatus(true); 
  88.                 axios.post('http://mock-api.com/mnE66LKJ.mock/getTableListData', { 
  89.                     ts: dataToppingStartNum, 
  90.                     ls: dataBottomOutStartNum 
  91.                 }).then(function(res) { 
  92.                     // 数据请求成功,改变表格加载层状态 
  93.                     setTableLoadingStatus(false); 
  94.                     if (res.status === 200) { 
  95.                         // 执行表格数据渲染函数 
  96.                         tableDataRendering(res); 
  97.                     } else { 
  98.                         alert("服务器错误"); 
  99.                     } 
  100.                 }); 
  101.             } 
  102.         } 
  103.     } 
  104. </script> 

这里需要比较坑的地方就是如果触顶/触底时,拖动横向滚动也会触发滚动监听,因此我们需要排除横向滚动事件。

【编辑推荐】

  1. 再见Excel!超强国产开源在线表格Luckysheet走红GitHub
  2. 再见Excel!最强国产开源在线表格Luckysheet走红GitHub
  3. 将你的日历与Ansible集成,以避免与日程冲突
  4. 无人公交提上日程 自动驾驶还需进步
  5. Python爬虫实战:采集淘宝商品信息并导入EXCEL表格
【责任编辑:武晓燕 TEL:(010)68476606】

点赞 0
分享:
大家都在看
猜你喜欢

订阅专栏+更多

云原生架构实践

云原生架构实践

新技术引领移动互联网进入急速赛道
共3章 | KaliArch

29人订阅学习

数据中心和VPDN网络建设案例

数据中心和VPDN网络建设案例

漫画+案例
共20章 | 捷哥CCIE

193人订阅学习

搭建数据中心实验Lab

搭建数据中心实验Lab

实验平台Datacenter
共5章 | ITGO(老曾)

119人订阅学习

视频课程+更多

HTML5CSS3JAVASCRIPT三合一教程实战

HTML5CSS3JAVASCRIPT三合一教程实战

讲师:张晨光24846人学习过

零基础入门小程序开发及商城项目实战(2020持续更新中)

零基础入门小程序开发及商城项目实战(2020持

讲师:大麦茶4863人学习过

Redis

Redis

讲师:白丁1094人学习过

订阅51CTO邮刊

点击这里查看样刊

订阅51CTO邮刊

51CTO服务号

51CTO官微