Vue-Gantt-chart甘特图实现拖拽功能

发布日期:2022-05-31文章字数:1.1K

去年做了一个展示任务计划的甘特图项目,我使用了Vue-Gantt-chart 这个插件来实现,要求实现拖拽功能还有一些别的需求,最近抽空把实现拖拽功能的代码重新整理了一下,发布出来。改动如下:

  • 样式调整,添加顶部部的时间刻度格和左侧日期。滚动插件使用 iscroll 实现,使滚动条样式在各浏览器下保持一致,支持鼠标按住拖动,类似手机上的按住滚动效果。

  • 数据分组:不同属性的甘特行可以分组,分组后数据渲染也是动态的,即只渲染浏览器视口内的数据,我本机测试万级数据(500行25列)轻微卡顿。

  • 数据搜索:搜索后高亮显示结果,并滚动到相应任务位置,若搜索到多个结果,继续点搜索按钮跳转到下一个结果。

  • 甘特块拖拽调整:基于浏览器原生拖拽事件实现,不同行之间的甘特块可以拖拽调整,调整时可以做一些校验,代码里暂时只做了时间校验,拖拽后默认会有一个黑色阴影块显示原来的任务,在配置项里可以设置为不显示,调整确认弹窗也可以选择显示或不显示(默认不显示)。

  • 右键菜单:若想要调整的行竖向间距过大不方便拖拽时,可使用右键菜单调整任务,可以选择复制或交换。

因为原插件的代码只包含了一个甘特图,添加了分组功能后,原来的动态渲染的代码就失效了,分组后的动态渲染功能我是这样实现的:

分组时给每一行的甘特数据添加一个rawIndex(原始索引)字段,甘特行的位置采用绝对定位,使用rawIndex来计算绝对定位的top值,直接rawIndex * 行高就是top值,这样即使上面的元素不显示也不影响渲染的元素位置。

然后使用getBoundingClientRect这个API来获取每个甘特组元素相对于浏览器窗口的top和bottom值,计算出需要显示的起始索引和结束索引。这是垂直方向上的动态渲染实现。水平方向上的动态渲染没有改动,是根据时间判断的。

判断的逻辑还是比较绕的,需要判断负值的情况,详细代码如下:


 /**
     * 分割出dom中需要显示的数据
     */
    sliceData() {
      if (!this.wrapperElement) return false;

      const {
        unVisibleHeight,
        heightOfBlocksWrapper,
        cellHeight,
        preload,
        datas
      } = this;

      const ClientRect = this.wrapperElement.getBoundingClientRect();
      const windowHeight = window.innerHeight;
      let startPosition = 0;
      let endPosition = 0;
      const top = ClientRect.top;
      const bottom = ClientRect.bottom;
      this.top = top;
      this.bottom = bottom;
      if (top <= 0) {
        startPosition = Math.abs(top) + unVisibleHeight;
        endPosition = startPosition + heightOfBlocksWrapper;
        if (bottom > unVisibleHeight && bottom <= windowHeight) {
          endPosition = startPosition + bottom;
        } else if (bottom <= unVisibleHeight) {
          this.startRenderNum = 0;
          this.endRenderNum = 0;
          return;
        }
      } else if (top > 0 && top <= unVisibleHeight) {
        startPosition = unVisibleHeight - top;
        endPosition = startPosition + heightOfBlocksWrapper;
      } else if (top > unVisibleHeight && top <= windowHeight) {
        startPosition = 0;
        endPosition = windowHeight - top;
      } else if (top > windowHeight) {
        this.startRenderNum = 0;
        this.endRenderNum = 0;
        return;
      }

      //没有高度,不需要渲染元素
      if (heightOfBlocksWrapper === 0 || cellHeight === 0) {
        this.startRenderNum = 0;
        this.endRenderNum = 0;
        return;
      }

      // 为 0 全部渲染
      if (preload === 0) {
        this.startRenderNum = 0;
        this.endRenderNum = datas.length;
        return;
      }
      const startRenderNum = Math.ceil(startPosition / cellHeight) - preload;
      this.startRenderNum = startRenderNum < 0 ? 0 : startRenderNum;

      const endRenderNum = Math.ceil(endPosition / cellHeight) + preload;
      this.endRenderNum =
        endRenderNum > datas.length ? datas.length : endRenderNum;
    }

以下图为例,假设红色框是浏览器的窗口,红框外面是一个甘特组,包含14行数据,每行高50像素,浏览器窗口高400,那么获取到的top和bottom就是-150和550,可以计算出渲染起始位置为:150-550索引值为中间8个:3-10。

2022_0605_120801.png

多个分组同时出现在窗口内时,如下图,上面的渲染起始位置为:100-300 索引值:2-5,下面的渲染起始位置为0-200,索引值为0-3。

2022_0605_120742.png

当然上面只是理想状态下的情况,如果一行的位置只滚动到一半也是不显示的,实际代码中需要加一个预渲染值,这里设置为1,起始和结尾都多渲染一列。

动图演示

拖拽移动

vue_drag_gantt_1.gif

数据分组

vue_drag_gantt_2.gif

搜索

vue_drag_gantt_3.gif

拖拽调整任务

vue_drag_gantt_4.gif

右键菜单调整任务

vue_drag_gantt_5.gif

demo: 在线演示

github地址: https://github.com/liyang5945/vue-drag-gantt-chart