背景
记录一次在项目开发过程中x6用法。
模块需求:
在线建模。
操作流程:
从左侧菜单中拖拽一个工具,显示在正中央的画布中,可在右侧面板对节点参数进行配置,底部面板展示工具运行日志。
效果图:
实现细节
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);
};
- 画布居中
const centerCanvas = () => {
graph.value.centerContent();
};
3.重置画布
// 重置画布
const handleReset = () => {
resetNode(user.nodeData); // 自定义方法,重置工具输入输出的位置
resetCanvas();// 自定义分方法
graph.value.zoomTo(1);// api,画布回到1倍大小
ElMessage.success("重置节点成功");
};
4.添加临时执行方法
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;
}
};
- 临时查看数据
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));
};
- 开始定时缓存页面数据
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()下载压缩包
- 导入模型
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清空页面所有的节点信息
- 设置节点状态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();