antv x6项目开发记录

841 阅读3分钟

背景

记录一次在项目开发过程中x6用法。

模块需求:

在线建模。

操作流程:

从左侧菜单中拖拽一个工具,显示在正中央的画布中,可在右侧面板对节点参数进行配置,底部面板展示工具运行日志。

效果图:

image.png

实现细节

x6节点的初始化

1.自定义节点样式

2.自定义连线样式

import { Graph, Shape } from "@antv/x6";

// 连接桩原点的样式
const portsStyle = {
  circle: {
    r: 3,
    magnet: true,
    stroke: "#ddd",
    strokeWidth: 2,
    fill: "#fff",
  },
};
// 连接桩
const ports = {
  groups: {
    in: {
      position: "left",
      attrs: portsStyle,
    },
    out: {
      position: "right",
      attrs: portsStyle,
    },
  },
  items: [
    {
      id: "id1",
      group: "in",
    },
    {
      id: "id2",
      group: "out",
    },
  ],
};

// html创建节点
const getNodeElement = (node, type) => {
  const label = node.getData().displayName;
  const states = node.getData().state;
  const dataTypeEnum = node.getData().dataTypeEnum;
  const warp = document.createElement("div"); //最外层
  warp.className = "state-box";
  const main = document.createElement("div"); //文字
  main.className = `${type}-text`;
  main.innerText = label;
  const leftTop = document.createElement("div"); //左上角icon
  leftTop.className = `${type}-left-top`;
  const state = document.createElement("div"); //执行状态
  state.className = `state-icon iconfont ${states}`;
  warp.appendChild(leftTop);
  warp.appendChild(main);
  warp.appendChild(state);
  return warp;
};

// tool
Shape.HTML.register({
  shape: "tool",
  width: 150,
  height: 42,
  html(node) {
    return getNodeElement(node, "tool");
  },
  ports: ports,
});

// input
Shape.HTML.register({
  shape: "input",
  width: 150,
  height: 42,
  html(node) {
    return getNodeElement(node, "resource");
  },
  ports: ports,
});

// output
Shape.HTML.register({
  shape: "output",
  width: 150,
  height: 42,
  html(node) {
    return getNodeElement(node, "result");
  },
  ports: ports,
});

// 注册连线
Graph.registerEdge("dash-edge", {
  inherit: "edge",
  connector: { name: "smooth" },
  attrs: {
    line: {
      stroke: "rgb(132, 205, 247)",
      strokeDasharray: 4,
      strokeWidth: 2,
    },
  },
});

总结:

1 使用Shape.HTML.register(),可以在画布中通过创建html元素的方式自用定义节点样式。

2)使用Graph.registerEdge("dash-edge", {})可以定义画布中的连线。

3.初始化节点的基本信息

 const initCanvas = () => {
    const graph = new Graph({
      container: document.getElementById("efContentBox"),
      panning: true,
      mousewheel: true,
      connecting: {
        snap: true,
        allowBlank: false, //是否允许连接到画布空白位置的点,默认为 true
        allowMulti: false,
        allowLoop: false,
        allowNode: false,
        allowEdge: false,
        connectionPoint: "anchor",
        createEdge: () => {
          return graph.createEdge({
            shape: "dash-edge",
          });
        },
        validateEdge({ edge, type, previous }) {
          const source = user.nodeData.nodeList.find(
            (i) => i.id == edge.source.cell
          );
          const target = user.nodeData.nodeList.find(
            (i) => i.id == edge.target.cell
          );
          if (source.data.pid != target.data.pid) {
            if (source.data.type == "输出" && target.data.type == "输入") {
              return true;
            }
          }
          return false;
        },
      },

      grid: {
        visible: true,
        type: "doubleMesh",
        args: [
          {
            color: "#eee", // 主网格线颜色
            thickness: 1, // 主网格线宽度
          },
          {
            color: "#ddd", // 次网格线颜色
            thickness: 1, // 次网格线宽度
            factor: 4, // 主次网格线间隔
          },
        ],
      },
    });
    return graph;
  };

总结:

根据项目需要,初始化画布, 基本配置参考官方api。

createEdge属性用于动态创建节点连线

validateEdge属性用于在连线的时候进行逻辑处理,是否支持节点之间连线

4.为节点绑定事件

// 绑定事件
const bindEvent = () => {
  // 开启定时存储,刷新页面后仍保留上一次操作结果
  saveParamsTimed(); 
  
  //监听键盘,如果是Delete键,且选中当前节点/连线,进行删除操作
  // 这个方法绑定在画布上,性能可能会更好
  window.addEventListener("keydown", handleKeyDown);
  
  // 离开页面,清空定时器timer,清空动态绑定的方法,存储最后一次操作数据
  window.addEventListener("beforeunload", saveParams);
  
  // 初始化画布,并把数据赋给当前变量graph
  graph.value = initCanvas();
 
  // 动态添加连线
  graph.value.on("edge:connected", ({ isNew, edge }) => {
    if (isNew) {
      ...
    }
  });
  
 // 记录当前节点位置,为当前节点添加高亮样式
  graph.value.on("node:mousedown", ({ e, node, view }) => {
    activeNode.value = node.position();//记录当前节点位置
    if (activeView.value) {//为当前节点添加高亮样式
      activeView.value.removeClass("my-class");
    }
    activeView.value = view;
    user.nodeData.active = node.getData();
  });
  
  
  // 松开鼠标,判断是否移动了节点,根据实际情况移动整个工具的节点位置
  graph.value.on("node:mouseup", ({ e, node, view }) => {
    const x = node.position().x - activeNode.value.x;
    const y = node.position().y - activeNode.value.y;
    if (x == 0 && y == 0) {
      view.addClass("my-class");
      user.hideRightPanel = false;
      nodeForm.value.nodeInit();
    } else {
      const prop = node.getData().type === "工具" ? "pid" : "id";
      user.nodeData.nodeList.forEach((el) => {
        if (el[prop] == node.id) {
          el.x = el.x + x;
          el.y = el.y + y;
        }
      });
      if (prop === "pid") {
        resetCanvas();
      }
    }
  });
  
  // 鼠标移动节点外部连线,则添加delete工具
  graph.value.on("edge:mouseenter", ({ edge }) => {
    const source = edge.source.cell;
    const target = edge.target.cell;
    if (!source.includes(target) && !target.includes(source)) {
      edge.addTools([
        {
          name: "button-remove",
          args: {
            distance: "50%",
            markup: [
              {
                attrs: {
                  r: 12,
                },
              },
            ],
            onClick: ({ e, cell, viw }) => {
              // 处理删除连线后的数据
              const data = user.nodeData
              const temp = source.split('-')[0] + '|'
              data.lineList = data.lineList.filter(i => i.from != source && i.to != target)
              data.nodeList.map(i => {
                if (i.data.to == target) {
                  i.data.to = ''
                }
                if (i.data.disabled && i.data.params.includes(temp)) {
                  i.data.params = ''
                  delete i.data.disabled
                }
              })
              graph.value.removeEdge(cell);
            },
          },
        },
      ]);
    }
  });
  
  // 鼠标离开连线,删除工具
  graph.value.on("cell:mouseleave", ({ cell }) => {
    cell.removeTools();
  });
  
  // 双击几点,对节点进行折叠展开操作。
  graph.value.on("node:dblclick", ({ e, x, y, node, view }) => {
    const data = node.getData();
    if (data.type === "工具") {
      const cell = user.nodeData.nodeList.find((i) => i.id == data.id);
      cell.data.isCollapse = !cell.data.isCollapse;
      user.nodeData.nodeList.forEach((el) => {
        if (el.pid == data.id) {
          el.data.isCollapse = cell.data.isCollapse;
        }
      });
      resetCanvas();
    }
  });
};

全局存储pinia

该模块涉及到组件比较分散,使用pinia存储全局数据是最有效的, pinia更改数据相对会比较方便

hideRightPanel: true,// 控制右面面板的显示与隐藏
hideBottomPanel: true,// 控制底部日志面板的显示与隐藏
nodeData: { // 重点,保存画布节点的所有信息
 active: null, // 当前节点信息
 name: [], // 记录页面的id
 nodeList: [],// 记录每个节点的信息
 lineList: [], // 记录每条连线的信息
 classNames: {}, // 记录每个节点的className,用于传参
},
nodeInputData: null, // 节点输入源数据
nodeOutputData: null, // 节点输出源数据
功能实现

1.画布方法缩小

const canvasZoom = (number) => {
  graph.value.zoom(number);
};
  1. 画布居中
const centerCanvas = () => {
  graph.value.centerContent();
};

3.重置画布

// 重置画布
const handleReset = () => {
  resetNode(user.nodeData); // 自定义方法,重置工具输入输出的位置
  resetCanvas();// 自定义分方法
  graph.value.zoomTo(1);// api,画布回到1倍大小
  ElMessage.success("重置节点成功");
};

4.添加临时执行方法

image.png

image.png

const addTempExecMethod = () => {
  setTimeout(() => {
    const arr = document.getElementsByClassName("icon-box");
    for (let i = 0; i < arr.length; i++) {
      arr[i].addEventListener("click", addEvent);
    }
  }, 1000);
};

鼠标放到画布上面,出现临时执行查看数据源查看执行结果等tooltip, 因为在自定义node节点中没办法添加方法,所以在渲染的时候为节点动态添加方法,销毁画布时,需要手动把方法卸载。

5.判断当前节点类型,判断是查看数据源,还是临时执行操作

const addEvent = () => {
  const activeNode = user.nodeData.active;
  switch (activeNode.type) {
    case "工具":
      tempExecute1(activeNode);
      break;
    case "输入":
    case "输出":
      tempShow();
      break;
  }
};
  1. 临时查看数据
const tempShow = () => {
  const data = user.nodeData;
  const activeNode = data.nodeList.find((i) => i.id === data.active.id);
  if (activeNode.data.type === "输入") {
    if (!activeNode.data.params) {
      ElMessage.warning("请先设置数据源");
      return;
    }
    showInputDS(activeNode.data);
  } else {
    if (!isRunTool.value) {
      ElMessage.warning("请先执行模型");
      return;
    }
    if (runType.value == "temp") {
      getTempDs(activeNode.data);
    } else if (runType.value == "formal" && activeNode.data.type != "内存") {
      getExecDs(activeNode.data);
    }
  }
};

根据当前activeNode在user中找到对应的节点信息。

如果是输入节点,没有填写数据源,则提示用户,参数完整则调用方法查询数据源

如果是输出节点,没有执行结果(isRuntool为空),则提示用户执行操作,然后判断一下是临时执行还是全局执行, 分别查看对应的执行结果。

7.查看输入数据源(业务逻辑,省略)

const showInputDS = (node) => {})

8.查看正式执行-输出数据源(业务逻辑,省略)

const getExecDs = (node) => {};

9.查看临时执行-输出数据源(业务逻辑,省略)

const getTempDs = (node) => {})

10.节点拖拽

<div id="efContentBox" @dragover="allowDrop($event)" @drop="addNode"></div>

允许拖拽

const allowDrop = (e) => {
  e.preventDefault();
};

节点拖放完毕,获取节点信息,然后开始渲染工作

const addNode = (e) => {
  const obj = JSON.parse(e.dataTransfer.getData("text/html")); // 节点信息
  obj.x = e.offsetX;//拖放位置
  obj.y = e.offsetY;
  obj.nodeId = Math.random().toString(36).slice(3, 10);
  getToolParameter({ id: obj.id }).then((res) => {// 业务逻辑
    if (res.code == 200) {
      const classNames = {
        [obj.nodeId]: {
          name: obj.name,
          className: obj.className,
        },
      };
      const { nodearr, edgearr } = getNode(obj, res.content);
      user.nodeData.classNames = { ...user.nodeData.classNames, ...classNames };
      user.nodeData.name.push(obj.nodeId);
      user.nodeData.nodeList.push(...nodearr);
      user.nodeData.lineList.push(...edgearr);
      graph.value.addNodes(nodearr);
      graph.value.addEdges(edgearr);
      addTempExecMethod();
    }
  });
};

graph.value.addNodes()动态添加节点,比重新渲染节点节省性能

graph.value.addEdges()动态添加连线,比重新渲染连线节省性能

addTempExecMethod为新添加的节点动态绑定方法

11.浏览器关闭或刷新,或离开当前页面时,保存数据,清除定时器

const saveParams = () => {
  clearInterval(timer.value);
  localStorage.setItem("temp", JSON.stringify(user.nodeData));
};
  1. 开始定时缓存页面数据
const saveParamsTimed = () => {
  timer.value = setInterval(() => {
    localStorage.setItem("temp", JSON.stringify(user.nodeData));
  }, 60000);
};

13.从localstorage读取数据

const getDataFromLocalstorage = () => {
  let a = JSON.parse(localStorage.getItem("cur_model"));
  if (a.origin.nodeList[0].data) {
    user.nodeData = a.origin;
  } else {
    user.nodeData = {
      active: null,
      name: a.origin.name,
      nodeList: changeNodeList(a.origin.nodeList),
      lineList: changeLineList(a.origin.lineList),
      classNames: changeClassName(a.execute.nodeList),
    };
  }
  executeInfo.value = a.execute;
  resetCanvas();
};

14.监听键盘操作,删除当前节点(业务逻辑,省略)

  if (e.code === "Delete") {
    const data = user.nodeData;
    // 当前节点了类型如果是工具,则删除,如果是工具下的输入输出,则忽略
    if (data.active && data.active.type == "工具") {
     ......
      user.hideRightPanel = true;
      resetCanvas();
    }
  }
};

删除节点后,右侧面板信息清空,隐藏右侧面板

resetCanvas()重新渲染当前节点

15.下载

const download = () => {
    window.open(`${Config.interface}/joblog/downfile?id=${isRunTool.value}`);
}

通过window.open()下载压缩包

  1. 导入模型
const importModule = (file) => {
  var reader = new FileReader(); //这里是核心!!!读取操作就是由它完成的。
  reader.readAsText(file.raw); //读取文件的内容
  reader.onload = function (result) {
    localStorage.setItem("cur_model", result.target.result);
    getDataFromLocalstorage();
  };
};

17.清空所有节点(业务逻辑,省略)

const clear = () => {
  graph.value.clearCells();
  ......
};

graph.value.clearCells();api清空页面所有的节点信息

  1. 设置节点状态arr=要设置的节点 state=状态 id=针对id设置状态
const setRunState = (arr, state, id) => {
  if (id) {
    arr.forEach((el) => {
      if (el.pid === id) {
        el.data.state = state;
      }
    });
  } else {
    arr.forEach((el) => {
      el.data.state = state;
    });
  }
  resetCanvas();
};

执行工具过程中,定时请求接口获取工具的执行情况

全局执行

--设置全部节点为loading状态, resetCanvas()重新渲染节点, 这部分会比较耗性能

--根据返回来接口数据,重置每个节点的状态信息,重新渲染节点。

临时执行

--设置临时执行部分节点loading状态,重新渲染节点

--根据接口返回来的值,重置节点状态。

19.保存

const publicModel = () => {
      graph.value.toPNG((dataUri) => {
        if (isEdit.value) {
          editModule(dataUri);
        } else {
          saveDialog.value.open(dataUri, isRunTool.value, executeInfo.value);
        }
      }, {
        padding: 20,
        stylesheet: exportModelCss(),
        copyStyles: false,
      })

    }
  }
};

实现保存功能前, 下载当前的流程图

dataUri:流程图的base64格式

padding: 设置图片的间隔

// stylesheet:设置导出节点的样式(如果不设置,节点会糊在一起)

stylesheet:` .main{ padding:20px} ...... `

copyStyles:是否忽略原有的样式

总结

模块中用到的antV x6的API有:

Shape.HTML.register() 自定义节点

Graph.registerEdge("dash-edge", {}) 自定义连线

new Graph() 初始化节点

graph.createEdge() 创建节点

graph.value.on("blank:click", ({ e, x, y }) => { }); 画布事件

graph.value.on("edge:connected", ({ isNew, edge }) => {})画布事件

graph.value.on("node:mousedown", ({ e, node, view }) => {});画布事件

graph.value.on("node:mouseup", ({ e, node, view }) => {})画布事件

graph.value.on("edge:mouseenter", ({ edge }) => {})画布事件

graph.value.on("cell:mouseleave", ({ cell }) => {})画布事件

graph.value.on("node:dblclick", ({ e, x, y, node, view }) => {})画布事件

graph.value.removeEdge(cell);移除边

graph.value.zoom(number);放大缩小

graph.value.centerContent();居中

graph.value.zoomTo(1);

graph.value.clearCells();

graph.value.addNodes(renderNodeList);

graph.value.addEdges(renderLineList);

graph.value.toPNG((dataUri) => { editModule(dataUri); }, { padding: 20, stylesheet: exportModelCss(), copyStyles: false, })

graph.value.clearCells();