likes
comments
collection
share

手把手教你 在 React 使用 dhtmlx - gantt 实现 甘特图(下)

作者站长头像
站长
· 阅读数 23

前期回顾

  • 本文档建议搭配 源码一起食用,因为有些代码过长的地方,没有贴到文档里。源码地址在文章最后

处理数据

  • 因为这一期需要对任务进行重排,所以我们在 formatTask 那个方法里将我们的数据处理成我们之后需要的集合

  • 首先需要改造一下 treeMap,上一期只需要实现渲染甘特图的渲染,而这一期我们需要在重排的时候能拿到 treeMap 中存储的任务原始数据

    • 需要将 startDateendDate 从字符串转换成 Date 对象,以及根据 进度生成 status 状态
    • 最后需要 cloneDeep 深拷贝一下,因为这样才不会被 dhtmlx-gantt 的更改任务方法污染原始数据
    • const tempData = mockData;
      
      tempData.data.forEach((item) => {
          // 如果 外部的开始日期和结束日期都有值,更新成 0 点即可
          const startDate = new Date(`${item.start_date} 00:00:00`);
          const endDate = new Date(`${item.end_date} 00:00:00`);
          endDate.setDate(endDate.getDate() + 1); // 在渲染页面的时候 结束日期 + 1天
          item.start_date = startDate;
          item.end_date = endDate;
      
        const status = item.progress === 1 ? "finish" : "continue";
          item.task_status = status;
      
          if (!taskTree[item.parent]) {
            taskTree[item.parent] = [];
          }
          taskTree[item.parent].push(item);
      });
      
      _treeMap.current = cloneDeep(taskTree);
      
  • codeMap 为 记录任务序号 以及 该任务有多少子任务 的集合

    • 因为 没有 0 这个父级,即 根任务的父级,所以这里虚拟一个 0 任务的映射,以便记录 根任务的个数
    • // 设置 codeMap
      // 因为在 treeMap 中没有最外层的数据,所以这里初始化一个最外层的对象
      const tempCodeMap = {
          "0": {
              code: null,
              count: taskTree["0"]?.length
          }
      };
      const newList = [];
      
      formatCodeMap(taskTree["0"], null, taskTree, tempCodeMap, newList);
      codeMap.current = { ...tempCodeMap };
      
      tempData.data = newList
      
      return tempData;
      
  • formatCodeMap任务data 处理成 codeMap 存取的格式

    • function formatCodeMap(
       items,
       parentCode,
       tree,
       tempCodeMap,
       newList,
       level = 0
      ) {
          if (!items) return;
          // 将 子代任务进行排序
          items.sort((a, b) => {
              return a.code - b.code;
          });
      
          // 遍历排序好的 子代
          items.forEach((item, index) => {
              const { id } = item;
              // 如果有 父级code,生成新的 code 为 父级code.父级子任务数量
              const code = parentCode
              ? `${parentCode}.${index + 1}`
              : String(index + 1);
              item.showCode = code;
              // 增加这三行 带$的属性 是为了 让甘特图新增完任务重排的时候顺序不乱
              item.$index = index;
              item.$level = level;
              if (broIndex) item.$rendered_parent = item.parent;
      
              // 将更新了 code 的 item 传出去更新
              newList.push(item);
      
              // 如果 tree[item.id] 存在,即为 该任务有子代,继续遍历
              if (tree[item.id]) {
                  tempCodeMap[id] = {
                      count: tree[item.id].length,
                      code
                  };
      
                  formatCodeMap(
                      tree[item.id],
                      code,
                      tree,
                      tempCodeMap,
                      newList,
                      level + 1
                  );
              } else {
                  tempCodeMap[id] = {
                      count: 0,
                      code
                  };
              }
          });
      }
      
  • 我们还需要在 formatTask 这个方法里去根据有前置任务的任务处理出任务之间的链接

    • 建立 targetMap 存储作为目标的任务 与 链接的映射
    • 建立 sourceMap 存储作为来源的任务 与 链接的映射
    • if (item.pre_task) {
      const source = item.pre_task;
      const target = item.id;
      
        const link = {
            id: `${source}-${target}`,
            type: "0",
            source,
            target
        };
        tempLinks.push(link);
        _targetMap.current[target] = link;
        _sourceMap.current[source] = link;
      }
      

添加新配置

  • 这一期我们会讲 任务的移动,这里先在 setGanttConfig 将任务移动需要的配置加上
  • 任务移动后的回调事件
    • 在任务在移动后触发 所有任务重新排序的方法
    • // 任务移动后的回调事件
      gantt.attachEvent("onAfterTaskMove", (id, parent, tindex) => {
          newUpdateSortCode(id, parent, tindex);
      });
      

重排任务并更新任务序号

手把手教你 在 React 使用 dhtmlx - gantt 实现 甘特图(下)

  • 在上期,我们在新建任务的时候使用到了 newUpdateSortCode 这个方法,刚好我们刚刚也在 setGanttConfig 这个方法里加入了移动后触发回调触发该方法的配置,即让任务进行重新排序
    • 在进行重排前,我们需要确定任务需要插入的位置

      • tindex0 时,即为插入这个层级的开头
      • tindex 等于 这个层级任务的数量 时,即为插入这个层级的最后
      • 其次我们要确定是否为同级拖拽,而且是否把自己拖到同级往后的位置
      • 如果上面两个条件有一个没满足,就属于正常的拖入,tindex 代表当前插入的位置。
        • 通过 tindex - 1 获取前一个任务的数据
          • 通过 tindex 获取原本在这个位置的任务,即插入后的后一个任务
      • 如果上面两个条件都满足了,即为同级拖拽,且将自身拖拽到后面的位置,那么因为在插入的时候,原本自己还占着位子,所以 tindex + 1 才是当前要插入的位置。
        • 现在就要通过 tindex 获取前一个任务的数据
        • 通过 tindex + 1 获取原本在这个位置的任务,即插入后的后一个任务
    • 这个方法用了 两个code,一个是用来展示的 showCode ,一个是用来真正排序的 隐式 code

      • 隐式code 是不进行重排计算的,因为他在同级中是唯一的
      • showCode 会根据 隐式code 排序好的顺序重新生成
    • 那么如何将 隐式code 保持到在同级中唯一?

      • 首先获取插入位置的前一个任务,获取其 code 小数点后的精度
      • 通过精度确定要在 前一个任务的 code 基础上增加的数值,精度为 1 时为 0.1,精度为 2 时为 0.01
      • 加完以后的 code 值不能等于插入位置 后一个任务的 code,如果相等,精度 + 1 后重新与前一个任务的 code 相加
        • 比如 前一个任务的 code0.1后一个任务的 code0.2
        • 根据 前一个任务的 code 确定精度为 1,增加数值为 0.1,相加得出的新 code0.2,但是等于后一个任务的 code
        • 所以精度 + 1,增加数值变为 0.01,重新加出来的 code0.11,保证了 code 的唯一,从而保证排序的时候顺序正确
        • 这个唯一性只需要在同级保持就行,因为其子级的 code 即使与自己的 code 一样,排序也排不到一起
      • 上面说的是插入两个任务之间的逻辑
      • 如果是插入到最后一位,直接将前一个任务的 code 加上其精度该加的数值
        • 比如 0.12 + 0.01 = 0.13
      • 而插入到第一位时,直接将其后一个任务的 code 的精度 + 1 后该加的数值作为 code
        • 比如后一个任务的 code0.1,精度为 1+ 1 后精度为 2,该加的数值为 0.01,那么 0.01 即为新 code
    •   // 新版  重排 任务用于排序的 code(隐式code 不重排,确保同级 code 唯一,然后显示code 只在前端渲染,给后端只传更改的数据)
        function newUpdateSortCode(id, parent, tindex, newTask, editTask) {
          ... 	// 获取其兄弟数组、原始父级、原始兄弟数组 等逻辑,代码过多,建议从源码中查看与复制
          
        // 判断 拖拽任务 拖拽前的父级 是否与 拖拽后的父级一样,并且 originIndex 是否小于 当前index
          const indexFlag = originParent === parent && originIndex < tindex;
          // 如果 indexFlag 为 true 的话 tindex 要比往常多 1,因为是同级拖拽,前面的数据一道后面时,index 不比平常多 1的话,会导致数据取的不对
          const beforeIndex = indexFlag ? tindex : tindex - 1;
          const afterIndex = indexFlag ? tindex + 1 : tindex;
          
        // 如果 拖拽到最后一个位置
          if (
            tindex > 0 &&
            tindex === (originParent === parent ? broList.length - 1 : broList.length)
          ) {
          ... 	// 计算精度的代码,可以在源码中查看与复制
         
            // 让 beforeCode 与 preNum 相加 即为 移动任务新的 code
            const moveCode = Number(
              BigNumber(beforeTask.code).plus(preNum).toString()
            );
          moveTask.code = moveCode;
          } else if (tindex > 0) { 	 // 如果不是在最后插入,并且 tindex > 0,即为在两个值之间插入了
            ... 	// 计算精度的代码,可以在源码中查看与复制
        
            // 如果 beforeCode + preNum === afterCode 时,需要提升精度 1 级精度
            if (
              BigNumber(preNum).plus(beforeTask.code).toString() ===
            `${afterTask.code}`
            ) {
              precision += 1;
              preNum = generateNumber(precision);
            }
          
          // 让 beforeCode 与 preNum 相加 即为 移动任务新的 code
            moveCode = Number(BigNumber(preNum).plus(beforeTask.code).toString());
            moveTask.code = moveCode;
          } else {  // 以上两个都不满足的话,即为插入到第一个的位置
        	... 	// 计算精度的代码,可以在源码中查看与复制
          
            // 根据 code 小数点后的数量确定 小数精度
            const precision = codeArr[1]?.length || 0;
          // 根据小数精度,确定需要增加的 Num 量
            const preNum = generateNumber(precision + 1);
            const moveCode = Number(preNum.toFixed(precision + 1));
            moveTask.code = moveCode;
          
            // 如果之前没有 broList,需要新建一个,并且更新到 tempTreeMap 中,用于之后添加
          if (!broList.length) {
              tempTreeMap[parent] = [];
              broList = tempTreeMap[parent];
            }
          }
          
          // 修改 移动任务的 parent 为 当前插入的 parent,并且编辑标识改为 true
          moveTask.parent = parent;
        
          // 将该任务 从原本存在的数组中 删除
          if (tempTreeMap[originParent])
          tempTreeMap[originParent].splice(originIndex, 1);
          // 在 要插入的数组中添加
          broList.splice(tindex, 0, moveTask);
          _treeMap.current = tempTreeMap;
          
          // 更新所有任务 以及 生成新的 codeMap
          updateCodeMapAndTask(tempTreeMap);
        }
      
    • 刚刚在上个方法将任务都重排完了,接下来在 updateCodeMapAndTask 这个方法里更新所有任务的序号

      • 使用 gantt.updateTask 更新任务
      • 使用 gantt.batchUpdate 批量更新,保证更新的性能
      • 更新完以后,使用 gantt.resetLayout 重置下甘特图即可
      • // 更新 codeMap 以及 重排任务的显示序号
        function updateCodeMapAndTask(treeMap) {
            const newList = [];
        
            // 因为在 treeMap 中没有最外层的数据,所以这里初始化一个最外层的对象
            const tempCodeMap = {
                "0": {
                    code: null,
                    count: treeMap["0"]?.length
                }
                };
        
                // 处理 任务成 codeMap, 并获得 更新过 code 的任务
                formatCodeMap(treeMap["0"], null, treeMap, tempCodeMap, newList);
            codeMap.current = tempCodeMap;
        
            // 批量更新任务
            gantt.batchUpdate(() => {
                newList.forEach((item) => {
                    gantt.updateTask(item.id, item);
                });
            });
            gantt.resetLayout();
        }
        

拖拽父级同时移动子任务

  • 手把手教你 在 React 使用 dhtmlx - gantt 实现 甘特图(下)

  • 首先,我们要确定 子级任务的开始日期 与 父级任务开始日期的 差值

    • 因为父级与子级要一起拖拽,那么他们拖拽的时候唯一不会变的就是他们开始日期之间的差值
    • // 设置 父子任务一起拖拽 以及 拖拽范围
      function setTaskDrag() {
          // 在拖拽前 获取 其与其子级开始日期的差值
          gantt.attachEvent("onBeforeTaskDrag", calcOffsetDuration);
      }
      
  • calcOffsetDuration 方法中计算父级与其所有子级的开始日期差值

    • 使用 gantt.eachTask 获取该任务的全部子级

    • 使用 gantt.calculateDuration 计算两个日期之间的持续时间

    • // 给其任务的子级 计算开始日期之间的工作日天数的差值
      function calcOffsetDuration(id) {
          const task = gantt.getTask(id);
      
          gantt.eachTask((child) => {
              const offsetDur = gantt.calculateDuration(
                  task.start_date,
                  child.start_date
              );
      
              child.offsetDur = offsetDur;
          }, id);
      
          return true;
      }
      
  • 当知道父级与子级开始日期的差值后,就可以实现拖拽逻辑了

    • 拖拽的时候,会有几种模式

      • move:任务移动,持续时间不变,开始日期和结束日期改变
      • resize:任务持续时间修改,开始日期和结束日期会有一个不变
      • progress:任务进度改变,其余不变
    • 那么现在是同时移动父级和子级,那即为 move 模式

      • // 这一个方法 是用来实现 父子一起拖动的
        gantt.attachEvent("onTaskDrag", (id, mode, task) => {
            const modes = gantt.config.drag_mode;
            const children = gantt.getChildren(id);
        
            // 当父级移动时
            if (mode === modes.move) {
                // 遍历所有子级
                gantt.eachTask((child) => {
                    const { offsetDur, duration } = child;
                    // 子级的开始日期为 父级开始日期 + 与父级开始日期的偏移量
                    const startDate = new Date(+task.start_date + offsetDur * 86400000);
                    // 子级的结束日期为 开始日期 + 持续时间,这里不重算休息日 是因为最后会重算
                    const endDate = new Date(+startDate + duration * 86400000);
        
                    // 设置子级数据并更新
                    child.start_date = startDate;
                    child.end_date = endDate;
        
                    gantt.refreshTask(child.id, true);
                }, id);
            }
        
            return true;
        });
        
    • 拖拽更新任务,使用的是 resize 模式,涉及到 resize 的时候多半需要限制任务更新的日期范围,所以我们放到下一节详细讲

    • 如果拖拽的是任务的进度,使用的就是 progress 模式

      • 手把手教你 在 React 使用 dhtmlx - gantt 实现 甘特图(下)

      • 代码很简单,只需要在拖拽完成后,更新下任务,因为在任务拖拽进度后,task 上的progress 属性已经更改了

      • // 如果 拖拽的是 进度
        if (mode === modes.progress) {
            gantt.updateTask(task.id, task);
        }
        

限制任务更新的日期范围

拖拽任务时限制

  • 手把手教你 在 React 使用 dhtmlx - gantt 实现 甘特图(下)
  • 在刚刚上面的 onTaskDrag 方法中,主要是以 父任务为主视角的,所以我们在刚刚的方法里,增加一段限制父级任务更新日期不能小于子任务的逻辑
    • else if (mode === modes.resize) {
          // 当父级修改范围时
          // 去获取子级的范围,父级无法缩小过子级的范围
          for (let i = 0; i < children.length; i += 1) {
              const child = gantt.getTask(children[i]);
              if (+task.end_date < +child.end_date) {
                  limitResizeLeft(task, child);
              } else if (+task.start_date > +child.start_date) {
                  limitResizeRight(task, child);
              }
          }
      }
      
  • 接下来要实现子任务拖拽移动不能超出父任务的日期范围
    • 手把手教你 在 React 使用 dhtmlx - gantt 实现 甘特图(下)

    • 因为 dhtmlx-gantt 的底层做了 eventListener 类似的处理,所以在使用 gantt.attachEvent 时,可以像用原生的 addEventListener 一样,写多个方法,会在底层合并到一起

    • 在上一个 onTaskDrag 方法中,我们以 父任务为主视角

    • 那么这一个 onTaskDrag 方法,我们以 子任务为主视角

      • 首先根据拖拽类型 确定要对 子任务使用的限制方法,即方法中的 limitMoveLeftlimitMoveRightlimitResizeLeftlimitResizeRight,分别对应 moveresize 模式的限制方法,具体方法可以到源码中查看并复制

        •   // 拖拽 到父级开始节点的限制
            function limitMoveLeft(task, limit) {
            	...		// 具体代码可以到源码中查看并复制
            }
              
            // 拖拽 到父级结束节点的限制
            function limitMoveRight(task, limit) {
            	...	    // 具体代码可以到源码中查看并复制
            }
            
            // 更改 子任务的开始时间 不能超过父任务的限制
            function limitResizeLeft(task, limit) {
              task.end_date = new Date(limit.end_date);
          }
              
            // 更改 子任务的结束时间 不能超过父任务的限制
            function limitResizeRight(task, limit) {
            task.start_date = new Date(limit.start_date);
            }
          
      • 确定了限制方法后,我们需要将子任务的日期 与 父级的任务日期进行比较

        • Tips:在比较的时候,将 Date 对象通过 +Date 的形式快速转换为时间戳会更好比较,比如 +parent.end_date
      • 如有超出,调用上面确定的 限制方法来保证 子任务的日期不超出父任务的范围

    • // 这一个 方法是 限制拖拽范围,因为他底层 应该是做了 类似 eventListener 之类的操作,所以可以写两个方法,差分开来显得清晰
      gantt.attachEvent("onTaskDrag", (id, mode, task) => {
          ... 	// 代码过多,可以到源码中查看与复制
      
           // 根据 Mode 设置限制范围的方法
          if (mode === modes.move) {
              limitLeft = limitMoveLeft;
              limitRight = limitMoveRight;
              const startDate = new Date(task.start_date);
              const endDate = gantt.calculateEndDate(startDate, task.duration);
      
              task.start_date = startDate;
              task.end_date = endDate;
          } else if (mode === modes.resize) {
              limitLeft = limitResizeLeft;
              limitRight = limitResizeRight;
          }
      
          // 将 parent 与 自己做判断
          // +Date 为快速转换为 时间戳的方式
          if (parent && +parent.end_date < +task.end_date) {
              limitLeft(task, parent);
          }
          if (parent && +parent.start_date > +task.start_date) {
              limitRight(task, parent);
          }
      
          return true;
      });
      

限制开始日期不为周末

  • 手把手教你 在 React 使用 dhtmlx - gantt 实现 甘特图(下)
  • 在任务做了限制以后,我们还需要看其限制完的开始日期是否在周末
    • 如果在周末,我们需要使用 delayStartDatedelayChildStartDate 方法延迟到下一个工作日

      • 这个两个方法在以后有改进的空间,现在只能跳过周末,但是以后可以通过判断跳过节假日
      • 两个方法的区别是 delayStartDate 给父级任务用,delayChildStartDate 给子级任务用,会多一个对工作日的遍历
      • // 延迟 开始日期,如果是 周末则跳到周一,TODO: 之后支持跳过节假日,跳到节假日后的第一个工作日
        export function delayStartDate(startDate) {
          const tempDate = new Date(startDate);
        
          while (tempDate.getDay() === 0 || tempDate.getDay() === 6) {
            tempDate.setDate(tempDate.getDate() + 1);
          }
        
          return tempDate;
        }
        
        // 延迟 子代的开始日期,duration 为与父级开始日期的 工作日天数差值
        export function delayChildStartDate(parentStart, duration) {
          // 设置一个 新变量,让其不污染 parentStart
          const tempDate = new Date(parentStart);
        
          // 循环遍历工作日
          for (let i = 0; i < duration; i += 1) {
            // 增加一天并跳过周末
            tempDate.setDate(tempDate.getDate() + 1);
        
            // 如果当前日期是周末,则跳过
            while (tempDate.getDay() === 0 || tempDate.getDay() === 6) {
              tempDate.setDate(tempDate.getDate() + 1);
            }
          }
        
          return tempDate;
        }
        
    • 延迟完开始日期后,就可以更新任务到视图上了

      • 利用 gantt.updateTask 更新任务
      • 再调用 updateTreeMapItem 方法更新 treeMap,利于之后做其他逻辑之后,拖拽任务限制时间的逻辑就大功告成了
    • // 拖拽完成后的 回调事件
      gantt.attachEvent("onAfterTaskDrag", (id, mode) => {
          const modes = gantt.config.drag_mode;
          // 获取 任务 和 父级
          const task = gantt.getTask(id);
          const parent =
                task.parent && task.parent !== 0 ? gantt.getTask(task.parent) : null;
      
          if (mode === modes.move) {
              // 如果 开始时间到节假日 延迟到下一个工作日
              const newStartDate = delayStartDate(task.start_date);
              const newEndDate = gantt.calculateEndDate(newStartDate, task.duration);
              task.start_date = newStartDate;
              task.end_date = newEndDate;
      
              // 如果父级存在,那么在拖动完后,进行重算时,不能超过父级的范围
              if (parent && +parent.start_date > +task.start_date) {
                  limitMoveRight(task, parent);
              }
              if (parent && +parent.end_date < +task.end_date) {
                  limitMoveLeft(task, parent);
              }
      
              // 遍历所有子级
              gantt.eachTask((child) => {
                  // 限制 任务子级的 开始日期和结束日期
                  controlChildLimit(child, task, newStartDate);
              }, id);
          } else if (mode === modes.resize) {
              const newStartDate = delayStartDate(task.start_date);
              const newEndDate = gantt.calculateEndDate(newStartDate, task.duration);
              task.start_date = newStartDate;
              task.end_date = newEndDate;
          }
      
          // 当任务拖拽更改后 只要不是新增的 就增加 edit 标识
          if (!task.isNew) {
              task.isEdit = true;
          }
          gantt.updateTask(task.id, task);
          updateTreeMapItem(task.parent, task.id, task);
          return true;
      });
        
      

模态框更新任务时限制

  • 但是除了拖拽更新的时候需要限制日期范围,在使用模态框更新也同样需要限制日期范围

    • 那么首先 我们在打开模态框的时候,需要使用 handleCalcMax 方法来计算 该任务持续时间的最大值。获取父级的结束日期,减去自己的开始日期,即为自己能增加到的最大持续时间
      • // 计算 持续时间最大值
        function handleCalcMax(task) {
            const record = { ...(task || curTask) };
            
            // 如果该任务为根任务,则不需要有最大值的限制
            if (!record.parent) {
              setMaxCount();
              return undefined;
            }
        
        	// 获取父任务的结束日期
            const parentTask = gantt.getTask(record.parent);
            const parentEndDate = new Date(parentTask.end_date);
        
            // 计算出 过滤了周末的 持续时间,即为 持续时间的最大值
            const startDate = new Date(record.start_date);
            const diffDay = gantt.calculateDuration(startDate, parentEndDate);
        
            setMaxCount(Number(diffDay));
            return Number(diffDay);
        }
        
    • 其次在上期我们在模态框的 handleFormChange 方法时,也需要用到这个方法
      • 如果开始日期变动了,我们需要重新计算

        • 手把手教你 在 React 使用 dhtmlx - gantt 实现 甘特图(下)
        • if (value.start_date) {
              const durationLimit = handleCalcMax(allValue);
              
              // 当 duration 上限存在 并且 duration 大于上限时, duration 等于上线
              if (durationLimit && duration > durationLimit) {
                  duration = durationLimit;
              }
          
              // 更新 duration
              formRef.current.setFieldsValue({
                  duration
              });
          }
          
      • 如果父任务变动了,也需要重新计算,但不止要重新计算任务持续时间,还需要看任务是否在新任务的时间范围内,如果不在还需要修改开始日期

        • // 如果 任务 不在父任务的范围内
          if (
              !(
                  allValue.end_date <= parentEndDate &&
                  allValue.start_date >= parentTask.start_date
              )
          ) {
              // 如果 任务原本的持续时间 大于 父任务的持续时间,任务的持续时间改为与父任务相等
              if (tempTask.duration > parentTask.duration) {
                  tempTask.duration = parentTask.duration;
              }
              
              // 获取父级的 startDate 并计算 任务修改到父任务日期范围内后的 endDate
              const startDate = parentTask.start_date;
              const endDate = gantt.calculateEndDate(startDate, tempTask.duration);
              endDate.setDate(endDate.getDate() - 1);
          
              // 重新更新 开始和结束日期
              tempTask.start_date = startDate;
              tempTask.end_date = endDate;
          
              formRef.current.setFieldsValue(tempTask);
          }
          
          handleCalcMax(tempTask);
          
  • 当然我们不止要在任务更新以后去限制持续时间,我们也要在选择开始日期前,限制能选择到的日期

    • 手把手教你 在 React 使用 dhtmlx - gantt 实现 甘特图(下)
    • date-picker 组件里,给一个 handleDisabledDate 方法
      • 将父级的任务日期作为可选择日期
      • 过于久远的日期不能选
      • 周末不能选
        • TODO:这个之后能做成节假日不能选,因为会有调休后的周末也得上班的情况
      • function handleDisabledDate(cur) {
            const formValue = formRef.current.getFieldsValue();
            let parentLimit = false;
        
            // 如果任务存在父级,将父级的任务日期作为可选择日期
            if (![null, 0, "0"].includes(formValue.parent)) {
                const parentTask = gantt.getTask(formValue.parent);
                const endDate = parentTask.end_date;
                endDate.setDate(endDate.getDate() - 1);
        
                parentLimit =
                    cur.isBefore(dayjs(parentTask.start_date)) ||
                    cur.isAfter(dayjs(endDate));
            }
        
            // 过于久远的日期也不能选择
            const lowerLimit =
                  cur.isBefore(dayjs("1970-01-01 00:00:00")) ||
                  cur.isAfter(dayjs("2038-01-01 00:00:01"));
        
            if (parentLimit) return true;
            if (lowerLimit) return true;
         
            // 周末也不能选
            // TODO: 这里之后可以搞成节假日,因为有些周末调休后也得上班
            if ([0, 6].includes(cur.day())) return true;
        }
        
  • 在上期我们预告了在模态框保存任务中增加限制任务的逻辑

    • gantt.eachTask((child) => {
          // 限制 任务子级的 开始日期和结束日期
          controlChildLimit(child, newTask, newTask.start_date);
      }, newTask.id);
      
    • 在任务保存的时候,限制所有子级的开始日期和结束日期
      • 和拖拽移动一样,与 父级任务开始日期的 差值是不会变的

      • 因为差值不变,我们就可以根据父级的开始日期,计算子级新的开始日期,如果新的开始日期在周末就可以用到之前说的 delayChildStartDate 方法去延迟子级的开始日期

      • // 限制 任务子级的 开始日期和结束日期
        function controlChildLimit(child, task, parentStart, noChangeFlag) {
            // 如果子级的开始时间到 节假日了,也需要往后延迟到工作日
            // 除此之外 还要和父级保持 相等的工作日天数差值
            const childStartDate = delayChildStartDate(parentStart, child.offsetDur);
        
            // 更新 子级任务的数据
            child.start_date = childStartDate;
            child.end_date = gantt.calculateEndDate(childStartDate, child.duration);
        
            // 更新任务
            child.isEdit = true;
        
            // 如果传了 这个参数 就不去实时更新
            // 主要是在 模态框里确定后使用的,在那里如果提前更新的话 会导致最后更新数据出现错行的问题
            if (!noChangeFlag) {
                gantt.updateTask(child.id, child);
                updateTreeMapItem(child.parent, child.id, child);
            }
        }
        

任务之间链接(前置任务)

  • 任务之间存在指向关系,比如任务A 是 任务B 的前置任务,任务A 则会指向任务 B

    • 手把手教你 在 React 使用 dhtmlx - gantt 实现 甘特图(下)
  • 首先我们需要设置链接变动后的回调函数

    • 使用 onAfterLinkAdd 回调方法,我们对链接添加后的逻辑进行处理

    • 有几个不能添加上链接的场景

      • 任务之间不能循环引用,比如 任务A 是 任务B 的前置任务,这时候原本存在一个 A 指向 B 的链接。再添加一个 B 指向 A 的链接就会导致任务之间指向循环,所以不允许
        • 手把手教你 在 React 使用 dhtmlx - gantt 实现 甘特图(下)
      • 任务不允许有多个前置任务,比如 任务A 是任务 B 的前置任务,这时候 任务C 也要成为任务 B 的前置任务时就会报错,因为 任务B 已经有 任务 A 这个前置任务了。
        • 手把手教你 在 React 使用 dhtmlx - gantt 实现 甘特图(下)
      • 不允许有重复链接,比如 任务A 是任务 B 的前置任务,这时候再添加一个 任务A 指向 任务 B 的链接,没有必要,纯多余,所以不允许
        • 手把手教你 在 React 使用 dhtmlx - gantt 实现 甘特图(下)
      • 以上这几个场景都为公司要求的,我觉得除了重复链接是必要的限制场景外,其他的场景可以根据需求进行取舍
    • 当链接可以被添加时,与在 formatTask 方法里差不多,将新链接分别存入与 来源任务 和 目标任务 映射的集合中,最后更新目标任务即可

      •   // 设置 link 链接变动的时候的 回调函数
          function setLinkChange() {
            // 链接添加后的回调事件
            gantt.attachEvent("onAfterLinkAdd", (id, link) => {
              const { target, source } = link;
              const newId = `${source}-${target}`;
              // 查找 targetMap 看该任务 存不存在 已有的链接
              const targetLink = _targetMap.current[target];
              const sourceLink = _targetMap.current[source];
              const nowLink = gantt.getLink(id);
        
              // 查找 来源节点的 link,如果 来源link的 source 等于 当前 target 时,代表任务循环引用了
              if (sourceLink && sourceLink?.source === target) {
                // 不一致且存在链接的时候,不允许他拖拽上
                if (nowLink) {
                  gantt.deleteLink(id);
                  message.warning(
                    "任务之间不能循环引用,该任务的前置任务不能是其后置任务!"
                  );
                  return true;
                }
              } else if (targetLink) {
                // 看一下是否和当前的 来源是否不一致 或 链接的来源为 这次的目标,这即为循环引用,不允许
                if (targetLink.source !== source) {
                  // 不一致且存在链接的时候,不允许他拖拽上
                  if (nowLink) {
                    gantt.deleteLink(id);
                    message.warning(
                      "该任务已有前置任务,如需关联,请先删除该任务的关联关系!"
                    );
                  }
                } else if (id !== newId) {
                  // 如果来源一致,即有可能重复链接了
                  if (nowLink) {
                    gantt.deleteLink(id);
                    message.warning("该任务已链接此前置任务,无需再关联一次!");
                  }
                }
              } else {
                // 如果不存在
                // 更新 新增好的 Link 的 id
                const newLink = { ...link, id: newId };
                _targetMap.current[target] = newLink;
                _sourceMap.current[source] = newLink;
                gantt.changeLinkId(id, newId);
        
                // 然后更新 目标组件
                const targetTask = gantt.getTask(target);
                targetTask.pre_task = String(source);
        
                targetTask.isEdit = true;
                gantt.updateTask(targetTask.id, targetTask);
                updateTreeMapItem(targetTask.parent, targetTask.id, targetTask);
              }
        
              return true;
            });
          }
        
    • 链接添加了以后,就得有链接删除的逻辑

      • 手把手教你 在 React 使用 dhtmlx - gantt 实现 甘特图(下)
      • 在 onAfterLinkDelete 方法中,进行判断 当链接的前置任务 与 映射集合中的相等,并且 id 相同,即允许删除
      • 配置完后,双击链接弹出提示确定即可删除
      •    // 链接删除后的回调函数
            gantt.attachEvent("onAfterLinkDelete", (id, item) => {
              const { target, source } = item;
              const newId = `${source}-${target}`;
              const preLink = _targetMap.current[target];
        
              // 如果 targetMap 中存在这个 link,并且 这个 id 是我们拼接好的 id,不是组件自己生成的 id 时 才去删掉
              if (preLink?.source === source && id === newId) {
                // 将其删掉
                delete _targetMap.current[target];
                delete _sourceMap.current[source];
        
                // 找到 link 指到的目标任务
                // 将该任务的 前置任务清空
                const targetTask = gantt.getTask(target);
                targetTask.pre_task = undefined;
        
                targetTask.isEdit = true;
                gantt.updateTask(targetTask.id, targetTask);
                updateTreeMapItem(targetTask.parent, targetTask.id, targetTask);
              }
            });
        
    • 完成了配置逻辑后,在图表中就可以直接拖拽链接生成了,不过还需要在模态框保存的时候,对链接进行增删

      • 手把手教你 在 React 使用 dhtmlx - gantt 实现 甘特图(下)
      • if (newTask.pre_task) {
            const { id, pre_task: preTask } = newTask;
            // 设置 link
            const tempLink = {
                id: `${preTask}-${id}`,
                source: preTask,
                target: id,
                type: "0"
            };
        
            // 如果 targetMap 中不存在,直接 添加 link
            if (!_targetMap.current[id]) {
                _targetMap.current[id] = tempLink;
                _sourceMap.current[preTask] = tempLink;
                gantt.addLink(tempLink);
            } else {
                // 如果 targetMap 中存在
                const preLink = _targetMap.current[id];
        
                // 看一下存的 source 是否和 当前前置任务一致,不一致的时候
                if (preLink.source !== preTask) {
                    gantt.deleteLink(preLink.id);
                    _targetMap.current[id] = tempLink;
                    _sourceMap.current[preTask] = tempLink;
                    gantt.addLink(tempLink);
                    newTask.pre_task = preTask;
                    // setDynFieldValue(newTask, 'pre_task', preTask);
                }
            }
        } else {
            // 如果保存的任务 没有配置前置任务
            const { id, pre_task: preTask } = newTask;
            const preLink = _targetMap.current[id];
        
            // 查看是否存在于  targetMap 中,如果存在,即这次为清空前置任务,需要删掉 link
            if (_targetMap.current[id]) {
                gantt.deleteLink(preLink.id);
                delete _targetMap.current[id];
                delete _sourceMap.current[preTask];
            }
        }
        

总结

  • demo 体验地址3h6qyr.csb.app/
  • 源码地址codesandbox.io/s/gantt-dem…
  • dhtml-gantt 案例地址
  • dhtml-gantt API 文档地址
  • dhtml-gantt React 中引用使用文档
  • 这一期结束后,基本甘特图的拖拽移动、拖拽更新、拖拽新增链接、限制任务时间的功能应该都能实现了。当然 dhtmlx-gantt 还不止这些功能,还有很多功能大家可以去尝试使用。
  • 这篇文章里也有很多还可以继续优化的,比如延迟开始时间应该跳过节假日的等等,因为本人在公司只需要用到上面这些功能,我就单独整理出了这篇文章。希望借我抛出的砖,可以引出大家的玉。
  • 之后如果有时间进行优化也会出一个番外篇来,欢迎大家积极发表意见。那么手把手教你在 react 使用 dhtmlx-gantt 实现甘特图先暂告一段落。
  • 文章中可能会有错误与遗漏,也欢迎大家指正与讨论。
转载自:https://juejin.cn/post/7276257954298691636
评论
请登录