在vue项目里用antv-X6来生成一张版本分支图(二)

1,367 阅读7分钟

接上篇:在vue项目里用antv-X6来生成一张版本分支图(一)

根据产品提的要求,布局改成了自下而上(BT)的布局走向,

因此路由方向的初始化也需要做对应的调整,

同时对节点进行一个分类,某一类需要展示在右侧,

dagre布局对于节点是根据nodes数组中index顺序渲染的,因此这边是请后端进行了数据处理,将某一类节点放在数组末尾,同时返回数据中添加节点类型的标志属性,用于前端给特定类节点设置不同方向的路由,这样就会达到特定类节点在右侧渲染的效果(但是实现效果有点一般,需要手动调整一下)

6. 配置高亮样式

对于当前节点的动画,高亮会一直保持,以及还有一些颜色的调整等等等

cellView.highlight(this.container,{
    highlighter:{
        name:'stroke',
        args:{
            attrs:{
                'stroke-width': 4,
                stroke: '#cd5454'
            }
        }
    }
})

7. 清除旧边上的动画

后续对动画效果进行了优化,在点击新的节点后会将由前节点产生的动画清除,留下新节点及其关联节点之前的动画,重复点击已有动画的节点则会取消该节点的动画

 //animation()中监听节点的点击事件
      this.graph.on("node:dblclick", async ({ node }) => {
        this.circleList =
          Array.prototype.slice.call(
            document.querySelectorAll('circle[id="circle"]'),
            0
          ) || [];
        if (
          JSON.stringify(this.cell) == JSON.stringify(node) &&
          this.circleList.length
        ) {
          this.unAnimation(node);
        } else {
          await this.triggerCell(node); //异步触发watch中监听的cell
          //trigger触发signer事件
          this.graph.trigger("signal", node);
        }
      });

异步触发watch中监听的cell

  triggerCell(node) {
      return new Promise((resolve) => {
        this.cell = node;
        setTimeout(resolve, 1000);
      });
    }

监听cell

  watch: {
    cell(cell, oldCell) {
      if (oldCell) {
        //消除线条上的动画和高亮
        this.unAnimation(oldCell);
      }
    },
  },

通过监听cell的变化/前后点击节点一致时调用的unAnimation()方法

 unAnimation(cell) {
      if (cell) {
        this.circleList =
          Array.prototype.slice.call(
            document.querySelectorAll('circle[id="circle"]'),
            0
          ) || [];
        if (this.circleList.length) {
          this.circleList.forEach((item) => {
            item.remove();
          });
        }
        this.unFlash(cell);  //取消节点高亮
      }
    },

8. 取消旧节点高亮

unFlash(cell) {
      let attrs = {
        highlighter: {
          name: "stroke",
          args: {
            attrs: {
              "stroke-width": 4,
              stroke: "#ce9178",
            },
          },
        },
      };
      const cellView = this.graph.findViewByCell(cell);
      if (cellView) {
        //返回节点的前序节点,即从根节点开始连接到指定节点的节点
        let cellList = this.graph.getPredecessors(cell);
        cellView.unhighlight(this.container, attrs);
        cellList.forEach((cell) => {
          const view = this.graph.findViewByCell(cell);
          view.unhighlight(this.container, attrs);
        });
      }
}

Ps:使用unhighlight()时需要注意其参数需要与highlight()保持一致

实现效果:

版本分支树取消动画 (2).gif

仍然没有结束哦!

又又又又又添加了新的需求(12.15更新)

9. 给节点添加气泡展示

在这个需求上走了不少弯路(菜鸟哭泣55555......)


下面是一些曲折经过,可自行跳过

最开始拿到这个需求后就在文档和api找提供的气泡配置,找了半天在G6发现了节点气泡的显示,但是在X6中没能找到(自己光盯着X6的文档和api瞅了,自动忽略了图表示例。。。)

于是想到自己来写一个DOM,监听节点,在鼠标进入节点后在对应节点里面插入DOM并渲染,这时遇到了第一个问题,节点经过渲染后变成了<svg>标签中的<g>标签,直接在<g>标签附近插入DOM的话是无效的,需要将DOM放入<foreignObject>⭐标签,里面有一个设置了xmlns="http://www.w3.org/1999/xhtml"命名空间的 <body>标签,此时<body>标签及其子标签都会按照XHTML标准渲染,实现了SVG和XHTML的混合使用

然后又遇到了第二个问题,渲染出来的块层级关系设置后无效,就会出现遮住了部分线条和节点但是也会被部分线条和节点遮住的现象(仍未找到原因及解决方式)

后来经过老大提醒

发现antv-x6的示例里面虽然没有提供在节点上的气泡工具,但是给节点上的连接桩提供了tooltip节点工具 | X6 (antgroup.com),以及在上也有tooltip辅助工具 | X6 (antv.vision),在鼠标进入连接桩或者边时会弹出气泡,不过存在一个问题就是鼠标离开连接桩后气泡会立马消失,这点不能满足点击气泡中的按钮实现跳转的需求

看完两个示例代码后发现它们大概的共同点是定义一个antdesigh提供的<Tooltip>组件进行渲染,然后使用document提供的api根据类名id找到tooltip组件DOM元素,在监听到相关事件后对其DOM位置等样式进行调整

于是我尝试了一下在vue中应用el-tooltip组件,然后在监听到节点相关事件后根据类名获取定义的DOM元素,其中el-tooltip组件光引用不行还需要先渲染出来后才能被获取到,这时又出现一个新的问题,如何在不使用鼠标移入已有文本的情况下将el-tooltip组件渲染出来(好像涉及到render函数进行强制渲染),目前还不知道怎么实现(菜菜)


最后决定将以上尝试的各种方式结合起来实现,自己用css画一个气泡,在节点事件被监听的时候渲染出来,并根据节点位置调整气泡位置,其余情况隐藏起来。

(在节点监听事件被触发时移动气泡位置可以避免进行大量DOM的增删操作)

开始上代码

先在容器HTML外(同级)定义一个<div>标签及其内部标签,用于展示气泡及相关内容,以及文本按钮

<div
      class="x6-tooltip"
      @mouseenter="mouseenterTooltip"
      @mouseleave="mouseleaveTooltip"
    >
      <div class="tooltip-content">
        <!-- 通过v-for和布局组件来展示气泡中的字段 -->
        <el-row v-for="item in versionLabel" :key="item.prop">
          <el-col :span="24">
            <div class="grid-content bg-purple-dark">
              {{ item.label }}
              <el-button
                v-if="item.prop === 'name'"
                type="text"
                @click="goPage(versionValue)">
                {{ versionValue[item.prop] }}</el-button>
              <span v-else>{{ versionValue[item.prop] }}</span>
            </div>
          </el-col>
        </el-row>
      </div>
    </div>

未监听到鼠标进入节点这个事件时是不希望气泡出现的,因此需要给该气泡宽高初始化为0

.x6-tooltip {
    min-width: 0;
    min-height: 0;
}

在画布初始化时监听鼠标移入节点这个事件,然后找到气泡DOM并调整其位置和样式

气泡与节点之间的距离的确定过程中也走了一些弯路

监听节点相关事件时可以拿到节点的position,但是这个position是节点初始化后画布未平移缩放时的位置,其不会随着画布的平移和缩放而变化,通过监听画布的平移和缩放拿到对应的偏移量和缩放值计算出来的位置还是不正确(数学也菜菜)

然后又发现监听鼠标点击节点的事件时可以拿到鼠标点击时位于视口的位置,使用之后又发现另一个问题,就是节点存在宽高,鼠标在节点内的存在一个运动区域,因此气泡的位置在同一个节点内位置是相对于鼠标位置决定的,这当然不能接受

最后是师父出手相助,向我展示了一个叫getBoundingClientRect()方法的使用方式(欢呼雀跃.jpg)

根据Element.getBoundingClientRect()方法获取到当前节点位于视口的位置,自定义一个偏移距离×画布缩放系数,从而可以实现节点与气泡之间的距离根据画布缩放比例自动调整

使用min-widthmin-height可以实现气泡宽度自适应,此时气泡的宽度是根据气泡内文本的长度来确定的

    //versionValue数据结构示例
 versionValue: { name: "node_2-1", label: "一些描述。。。。" },
 //监听画布缩放
      this.graph.on("scale", ({ sx, sy, ox, oy }) => {
        this.removeTooltip();
        this.scaleX = sx;
      });
//监听鼠标移入节点
      this.graph.on("node:mouseenter", ({ node }) => {
        let nodeDom = document.querySelector(`[data-cell-id=${node.id}]`);
        let nodePosition = nodeDom.getBoundingClientRect();
        console.log("nodePosition", nodePosition);
        let tooltip = document.getElementsByClassName("x6-tooltip")[0];
        if (tooltip) {
          let style = tooltip.style;
          style.backgroundColor = "#fef8e9";
          style.zIndex = "999";
          style.position = "fixed";
          style.boxShadow = "0 0 3px #d2d2d2";
          style.minWidth = "150px";
          style.minHeight = "50px";
          style.borderRadius = "5px";
          style.padding = "8px";
          style.fontSize = "14px";
          style.left = `${nodePosition.left + 145 * this.scaleX}px`; //this.scaleX是通过监听画布缩放获取的到缩放系数
          style.top = `${nodePosition.top}px`;
        }
        //气泡中label文本的数据结构
        this.versionLabel = [
          {
            prop: "name",
            label: "名称:",
          },
          {
            prop: "label",
            label: "描述:",
          },
        ];
        //调用获取气泡数据的请求
        // this.versioninfo(node.id);
      }),

现在气泡的展示问题基本算是解决了,最后还剩下气泡的移出,接下来就很简单了

10. 气泡的隐藏

先来解决如何让气泡消失

removeTooltip() {
      let tooltip = document.getElementsByClassName("x6-tooltip")[0];
      if (tooltip) {
        let style = tooltip.style;
        style.minWidth = 0;
        style.minHeight = 0;
        style.borderRadius = 0;
        style.padding = 0;
        this.versionLabel = [];
      }
    },

然后就是解决在什么情况下气泡需要消失: 在画布缩放时、平移时、点击画布空白区域时、页面跳转前、组件销毁时等等等

什么事件中不希望气泡存在就在之前监听该事件调用removetooltip()即可

 //监听鼠标移出节点
        this.graph.on("node:mouseleave", ({ node }) => {
          this.timer = setTimeout(() => {
            this.removeTooltip();
          }, 300);
        });
        
    //监听鼠标移出气泡
    mouseleaveTooltip() {
      this.removeTooltip();
    },

当鼠标进入气泡时,气泡不应该被隐藏,因此需要将定时器清掉

   //监听鼠标移入气泡
    mouseenterTooltip() {
      clearTimeout(this.timer);
    },

实现效果:

QQ录屏20230119173631.gif