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

本文转载自微信公众号「神奇的使用实现程序员K」,作者神奇的表格表程序员K 。转载本文请联系神奇的组件程序员K公众号。

 前言

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

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

环境搭建

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

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

<script crossOrigin type="text/javascript" src="lib/react.production.min.js"></script>    <script crossOrigin type="text/javascript" src="lib/react-dom.production.min.js"></script>    <script src="lib/babel.min.js"></script>    <script type="text/javascript" src="lib/moment.min.js"></script>    <script src="lib/lodash.min.js"></script>    <script type="text/javascript" src="lib/antd.min.js"></script>    <script type="text/javascript" src="lib/axios.min.js"></script>    <link rel="stylesheet" href="lib/antd.min.css"> 

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

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

<script type="text/babel">     console.log("react");     console.log(React);     console.log("antd")     console.log(antd); </script> 

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

image-20201119155715157

接下来,我们写个HelloWord来测试下效果。亿华云计算

<div id="root" style="width: 94%;overflow: hidden"></div> <script type="text/babel">     // 自定义hook     const App = () => {          const onChange = (date, dateString) => {              console.log(date, dateString);         }         return (             <div>                 React+antd引入成功                 <br />                 <antd.DatePicker onChange={ onChange} />             </div>         );     };     ReactDOM.render(<App />, document.getElementById("root")); </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中添加一条数据就行了,如下所示:

const App = () => {          const [columns, setColumns] = React.useState([]);         const [optRecords, setOptRecords] = React.useState([]);            //增加按钮函数         const btnClick = (e) => {              index++;             let columnsObj = {                  dataIndex: rcnr + (index),                 title: 日程内容 + index,                 align: center,                 onCell: tdSet,                 render: rctd_render,             }             // 表格列新增一列             columns.push(columnsObj)             setColumns(columns);             // 处理表格数据             for (let i = 0; i < optRecords.length; i++) {                  let key = "rcnr"+index;                 // 表格数据新增一条                 optRecords[i][key] = { text:"", code:"0"}             }             setOptRecords(optRecords);         }  } 

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

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

const App = () => {         const [optRecords, setOptRecords] = React.useState([]);        const [columns, setColumns] = React.useState([]);           //增加按钮函数        const btnClick = (e) => {             if (tableLoadingStatus) {                 alert("表格数据尚未加载完成");                return false;            }            columnsIndex++;            let columnsObj = {                 dataIndex: "rcnr" + (columnsIndex),                title: "日程内容" + columnsIndex,                align: "left",                className: "rcnrfontSet",                width: 189.5,                onCell: tdSet,                render: rctd_render            };            // 表格列新增一列            setColumns((arr => [...arr, columnsObj]));            // 处理表格数据            setOptRecords((arr) => arr.map((item) => {                 return {  ...item, ["rcnr" + columnsIndex]: {  wz: columnsIndex - 1 } };            }));        }; } 

表格列补齐

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

// 表格数据渲染函数         const tableDataRendering = function(res) {            // 获取最大子节点的key数量             let maxChildLength = Object.keys(defaultData[0].children[0]).length;             for (let i = 0; i < defaultData.length; i++) {                  for (let j = 0; j < defaultData[i].children.length; j++) {                      const currentObjLength = Object.keys(defaultData[i].children[j]).length;                     if (currentObjLength > maxChildLength) {                          maxChildLength = currentObjLength;                     }                 }             }             // 补齐缺少的节点             for (let i = 0; i < defaultData.length; i++) {                  for (let j = 0; j < defaultData[i].children.length; j++) {                      const currentObjLength = Object.keys(defaultData[i].children[j]).length;                     // 当前节点的长度小于第一个子节点的长度就补齐                     for (let k = currentObjLength; k < maxChildLength; k++) {                          defaultData[i].children[j]["rcnr" + k] = { };                     }                 }             }             // 如果存在空对象添加位置字段             for (let i = 0; i < defaultData.length; i++) {                  for (let j = 0; j < defaultData[i].children.length; j++) {                      // 获取每天的时间段对象                     const item = defaultData[i].children[j];                     // 获取所有的key                     const keys = Object.keys(item);                     // 提取所有的日程字段                     for (let k = 1; k < keys.length; k++) {                          // 日程为空添加wz字段                         if (Object.keys(item[keys[k]]).length <= 1) {                              defaultData[i].children[j][keys[k]].wz = k - 1;                         }                     }                 }             }         } 

监听子窗口关闭

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

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

<script type="text/babel">       // 声明代理变量     let pageStateEngineer;     // 需要进行代理的对象     let pageState = {  status: false };     // 监听子页面关闭,弹窗页面在关闭时可调用这个方法,触发页面刷新     const getSubpageData = (status) => {          console.log("子页面关闭");         pageStateEngineer.status = true;     };     const App = () => {          // 代理处理函数         const pageStateHandler = {              set: function(recObj, key, value) {                  // 表格状态改为正在加载                 setTableLoadingStatus(true);                 // 重新请求接口,获取最新数据                 axios.post(http://mock-api.com/mnE66LKJ.mock/getTableListData, {                  }).then(function(res) {                      // 数据请求成功,改变表格加载层状态                     setTableLoadingStatus(false);                     if (res.status === 200) {                          // 执行表格数据渲染函数                         tableDataRendering(res);                     } else {                          alert("服务器错误");                     }                 });                 // 修改对象属性                 recObj[key] = value;                 return true;             }         };         // 第一次渲染时,在借口调用成功后创建proxy         React.useEffect(() => {              // 调用接口获取表格数据             axios.post(http://mock-api.com/mnE66LKJ.mock/getTableListData, {                  ls: 0,                 ts: 0             }).then(function(res) {                  //创建代理,监听pageState对象改变,pageStateHandler处理变更                 pageStateEngineer = new Proxy(pageState, pageStateHandler);             })         }     } </script> 

重新渲染表格

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

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

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

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

const App = () => {          // 表格列格式定义         const defaultColumns = [             {                  dataIndex: "rq",                 title: "日期",                 align: "center",                 fixed: "left",                 colSpan: 2,                 width: 140.5,                 className: "rqfontSet",                 onCell: dateHandle,                 render: (value, item, index) => { }             },             {                  dataIndex: "sjd",                 title: "时间段",                 width: 70,                 colSpan: 0,                 fixed: "left",                 align: "center",                 className: "sjdfontSet",                 render: (value, item, index) => {                      let v1 = value.charAt(0);                     let v2 = value.charAt(1);                     return <div>{ v1}<br />{ v2}</div>;                 }             }         ];         // 表格数据渲染函数         const tableDataRendering = function(res) {            // 根据日程列字段数据赋值表格列的日程字段,rcList中包含sjd所以需要1开始             for (let i = 1; i < rcList.length; i++) {                  let rcnr = {                      dataIndex: rcList[i],                     title: "日程内容" + i,                     align: "left",                     width: 189.5,                     className: "rcnrfontSet",                     onCell: tdSet,                     render: rctd_render                 };                 defaultColumns.push(rcnr);             }             // 渲染表格数据             handleData(defaultData);             // 渲染表格列,使用cloneDeep进行深拷贝,触发useState的更新             setColumns(_.cloneDeep(defaultColumns));         }      // 计算要合并的列数         const handleData = (data) => {              if (data == null) {                  data = defaultData;             }             let newArr = [];             data.map(item => {                  if (item.children) {                      item.children.forEach((subItem, i) => {                          let obj = {  ...item };                         Object.assign(obj, subItem);                         delete obj.children;                         obj.rowLength = item.children.length;                         newArr.push(obj);                     });                 }             });             // console.log("处理好的表格数据");             // console.log(newArr);             // 将处理好的数据放入optRecords,使用cloneDeep进行深拷贝,触发useState的更新             setOptRecords(_.cloneDeep(newArr));         };   } 

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

触顶/触底加载数据

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

实现代码如下:

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

<script type="text/babel">     // 触顶数据起始条数     let dataToppingStartNum = 0;     // 触底数据起始条数     let dataBottomOutStartNum = 30;     // 横向/垂直滚动条起始位置     let levelPosition;     let verticalPosition;     // 触底/触顶次数     let topFrequency = 0;     let bottomFrequency = 0;     const App = () => {          // 横向滚动条位置         levelPosition = document.querySelector(".ant-table-body").scrollLeft;         // 纵向滚动条位置         verticalPosition = document.querySelector(".ant-table-body").scrollTop;         // 获取表格容器         let antdTable = document.querySelector(".ant-table-body");         //页面滚动监听         antdTable.onscroll = function() {              // 触底向后加载数据             if (antdTable.scrollTop + antdTable.clientHeight >= antdTable.scrollHeight) {                  // 判断是否横向滚动                 if (antdTable.scrollLeft !== levelPosition) {                      // 更新位置                     levelPosition = antdTable.scrollLeft;                     return false;                 }                 // 第一次触底不触发数据加载                 if (bottomFrequency === 0) {                      bottomFrequency++;                     return false;                 }                 if (bottomFrequency > 0) {                      bottomFrequency = 0;                 }                 dataBottomOutStartNum += 30;                 // 判断已加载的数据                 if (dataBottomOutStartNum > 90) {                      alert("最多只能向后加载90天的数据");                     return false;                 }                 // 保留向上滑动的天数                 let bottomTS = 0;                 // 页面第一次向上滑动,修改位置                 if (dataToppingStartNum !== 0) {                      bottomTS = -30;                 }                 setTableLoadingStatus(true);                 axios.post(http://mock-api.com/mnE66LKJ.mock/getTableListData, {                      ts: bottomTS,                     ls: dataBottomOutStartNum                 }).then(function(res) {                      // 数据请求成功,改变表格加载层状态                     setTableLoadingStatus(false);                     if (res.status === 200) {                          // 执行表格数据渲染函数                         tableDataRendering(res);                     } else {                          alert("服务器错误");                     }                 });             }             // 触顶向前加载数据             if (antdTable.scrollTop === 0) {                  // 判断是否横向滚动                 if (antdTable.scrollLeft !== levelPosition) {                      // 更新位置                     levelPosition = antdTable.scrollLeft;                     return false;                 }                 // 第一次触顶不触发数据加载                 if (topFrequency === 0) {                      topFrequency++;                     return false;                 }                 if (topFrequency > 0) {                      topFrequency = 0;                 }                 dataBottomOutStartNum += 30;                 if (dataBottomOutStartNum > 90) {                      alert("最多只能向前加载90天的数据");                     return false;                 }                 dataToppingStartNum -= 30;                 setTableLoadingStatus(true);                 axios.post(http://mock-api.com/mnE66LKJ.mock/getTableListData, {                      ts: dataToppingStartNum,                     ls: dataBottomOutStartNum                 }).then(function(res) {                      // 数据请求成功,改变表格加载层状态                     setTableLoadingStatus(false);                     if (res.status === 200) {                          // 执行表格数据渲染函数                         tableDataRendering(res);                     } else {                          alert("服务器错误");                     }                 });             }         }     } </script> 

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

人工智能
上一篇:便宜域名使用如何?小白可以买到便宜域名吗?
下一篇:4、企业无形资产:通用网站已成为企业网络知识产权的重要组成部分,属于企业的无形资产,也有助于提升企业的品牌形象和技术领先形象。它是企业品牌资产不可或缺的一部分。