因为公司业务需求,需要搞个流程图,用于展示每个节点的详情。在网上查了好多资料,选来选去最终选择了阿里的Antv x6(别问,问就是好看)
先上效果图
一、左侧自定义图形菜单
<div id="stencil"></div>
首先安装Avtv x6
npm install @antv/x6 --save
页面中直接引用
import { Graph, Shape, Addon, DataUri } from "@antv/x6";
data函数变量声明:
data(){
retrun {
stencil: null, //左侧菜单对象
graph: null, //画布
timeEnd: null,
time: null,
clickNode: {
//点击节点对象
customAttrs: {
clickNodeContent: "", // 内容
clickNodeContentSize: "", // 字体大小
clickNodeBoxSizeWidth: "", // 盒子宽度
clickNodeBoxSizeHeight: "", // 盒子高度
clickNodeBgColor: "", // 背景颜色
clickNodeTextColor: "", // 文字颜色
clickNodeBorderColor: "", // 边框颜色
positionx: 0, // x位置
positiony: 0, // y位置
},
},
clickNodeTemp: null,
drawer: false, //节点编辑抽屉
direction: "rtl", //抽屉从右往左打开
}
}
//初始化左侧图形菜单
this.initCanvas();//初始化画布
this.stencil = new Addon.Stencil({
target: this.graph,
stencilGraphWidth: 200,
stencilGraphHeight: 180,
groups: [
{
title: "基础流程图",
name: "group1",
},
],
layoutOptions: {
columns: 2,
columnWidth: 80,
rowHeight: 55,
},
});
let appendChildTemp = document.getElementById("stencil");
if (appendChildTemp) {
appendChildTemp.appendChild(this.stencil.container);
}
//初始化自定义图形
initCustomGraph() {
//初始化连接线能连接的点
const ports = {
groups: {
top: {
position: "top",
attrs: {
circle: {
r: 4,
magnet: true,
stroke: "#5F95FF",
strokeWidth: 1,
fill: "#fff",
style: {
// 设置隐藏,在通过事件鼠标移动显示
visibility: "hidden",
},
},
},
},
right: {
position: "right",
attrs: {
circle: {
r: 4,
magnet: true,
stroke: "#5F95FF",
strokeWidth: 1,
fill: "#fff",
style: {
visibility: "hidden",
},
},
},
},
bottom: {
position: "bottom",
attrs: {
circle: {
r: 4,
magnet: true,
stroke: "#5F95FF",
strokeWidth: 1,
fill: "#fff",
style: {
visibility: "hidden",
},
},
},
},
left: {
position: "left",
attrs: {
circle: {
r: 4,
magnet: true,
stroke: "#5F95FF",
strokeWidth: 1,
fill: "#fff",
style: {
visibility: "hidden",
},
},
},
},
},
items: [
{
group: "top",
},
{
group: "right",
},
{
group: "bottom",
},
{
group: "left",
},
],
};
Graph.registerNode(
"custom-polygon",
{
inherit: "polygon",
width: 66,
height: 36,
markup: [
{
tagName: "polygon",
selector: "body",
},
{
tagName: "text",
selector: "label",
},
],
attrs: {
body: {
strokeWidth: 1,
stroke: "#5F95FF",
fill: "#EFF4FF",
},
text: {
fontSize: 12,
fill: "#262626",
},
},
ports: {
...ports,
items: [
// 这里是限制连接点多少个的地方
{
group: "top",
},
{
group: "bottom",
},
],
},
},
true
);
Graph.registerNode(
"custom-circle",
{
inherit: "circle",
width: 45,
height: 45,
markup: [
{
tagName: "circle",
selector: "body",
},
{
tagName: "text",
selector: "label",
},
],
attrs: {
body: {
strokeWidth: 1,
stroke: "#5F95FF",
fill: "#EFF4FF",
},
text: {
fontSize: 12,
fill: "#262626",
},
},
ports: { ...ports },
},
true
);
Graph.registerNode(
"custom-rect",
{
inherit: "rect",
width: 66,
height: 36,
markup: [
{
tagName: "rect",
selector: "body",
},
{
tagName: "text",
selector: "label",
},
],
attrs: {
body: {
strokeWidth: 1,
stroke: "#5F95FF",
fill: "#EFF4FF",
},
text: {
fontSize: 12,
fill: "#262626",
},
},
ports: { ...ports },
},
true
);
const r1 = this.graph.createNode({
shape: "custom-rect",
label: "开始",
attrs: {
body: {
rx: 20,
ry: 26,
},
},
});
const r2 = this.graph.createNode({
shape: "custom-rect",
label: "过程",
});
const r3 = this.graph.createNode({
shape: "custom-rect",
attrs: {
body: {
rx: 6,
ry: 6,
},
},
label: "可选过程",
});
const r4 = this.graph.createNode({
shape: "custom-polygon",
attrs: {
body: {
refPoints: "0,10 10,0 20,10 10,20",
},
},
label: "决策",
});
const r5 = this.graph.createNode({
shape: "custom-polygon",
attrs: {
body: {
refPoints: "10,0 40,0 30,20 0,20",
},
},
label: "数据",
});
const r6 = this.graph.createNode({
shape: "custom-circle",
label: "连接",
});
this.stencil.load([r1, r2, r3, r4, r5, r6], "group1");
const showPorts = (ports, show) => {
for (let i = 0, len = ports.length; i < len; i = i + 1) {
ports[i].style.visibility = show ? "visible" : "hidden";
}
};
// 移入节点
this.graph.on("node:mouseenter", () => {
const container = document.getElementById("container"); // 这里获取id 是你画板的id
if (container) {
const ports = container.querySelectorAll(".x6-port-body");
showPorts(ports, true);
}
});
// 移出节点
this.graph.on("node:mouseleave", () => {
const container = document.getElementById("container"); // 这里获取id 是你画板的id
if (container) {
const ports = container.querySelectorAll(".x6-port-body");
showPorts(ports, false);
}
});
},
至此左侧图形菜单已完成。
二、右侧(工具栏+画布)
工具栏:
<div class="topTool">
<div class="tool" @click="undo">
<i class="el-icon-refresh-left"></i>
<div>撤 销</div>
</div>
<div class="tool" @click="clear">
<i class="el-icon-refresh"></i>
<div>清 空</div>
</div>
<div class="tool" @click="downImg">
<i class="el-icon-picture-outline"></i>
<div>导出图片</div>
</div>
<div class="tool" @click="save">
<i class="el-icon-finished"></i>
<div>保 存</div>
</div>
//撤销
undo() {
if (!this.graph.isHistoryEnabled()) {
}
// if (this.graph.isHistoryEnabled()) {
// this.graph.disableHistory();
// } else {
// this.graph.enableHistory();
// }
},
//清空
clear() {
this.$confirm("是否确定清空当前画布内容?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}).then(() => {
this.graph.fromJSON({});
});
},
//导出图片
downImg() {
this.graph.toPNG(
(dataUri) => {
// 下载
DataUri.downloadDataUri(dataUri, "chart.png");
},
{
padding: {
top: 30,
right: 30,
bottom: 30,
left: 30,
},
}
);
},
//保存
save() {
//页面图形转换成json
let preservationData = this.formatGraphData();
localStorage.setItem("graphCacheData", JSON.stringify(preservationData));
console.log(preservationData);
},
//格式化最终保存数据
formatGraphData() {
let data = this.graph.toJSON();
const model = {
nodes: [],
edges: [],
};
let tmp;
if (data.cells) {
tmp = data.cells;
} else if (data.nodes || data.edges) {
tmp = [].concat(data.nodes, data.edges);
}
if (tmp) {
tmp.forEach((item) => {
if (item.shape !== "edge") {
model.nodes.push(item);
} else {
let sourceId = item.source;
let targetId = item.target;
model.edges.push({
source: sourceId,
target: targetId,
connector: item.connector,
attrs: item.attrs,
router: item.router,
labels: item.labels,
});
}
});
}
return model;
}
画布
<div id="container"></div>
// 初始化画布
this.graph = new Graph({
container: document.getElementById("container"),
grid: true,
width: 1240, // 这个是自己定义的宽度
height: 800, // 这个是自己定义的高度
background: {
color: "#fff", // 设置画布背景颜色
},
mousewheel: {
enabled: true,
zoomAtMousePosition: true,
modifiers: "ctrl",
minScale: 0.5,
maxScale: 3,
},
panning: {
enabled: true, // 单独开启拖动操作
modifiers: "shift", // 按下shift才可以拖动
},
connecting: {
router: {
name: "manhattan",
args: {
padding: 1,
},
},
connector: {
name: "rounded",
args: {
radius: 8,
},
},
anchor: "center",
connectionPoint: "anchor",
allowBlank: true,
snap: true,
createEdge() {
return new Shape.Edge({
attrs: {
line: {
stroke: "#A2B1C3",
strokeWidth: 2,
targetMarker: {
name: "block",
width: 12,
height: 8,
},
},
},
zIndex: 0,
});
},
validateConnection({ targetMagnet }) {
return !!targetMagnet;
},
},
highlighting: {
magnetAdsorbed: {
name: "stroke",
args: {
attrs: {
fill: "#5F95FF",
stroke: "#5F95FF",
},
},
},
},
resizing: true,
rotating: true,
selecting: {
enabled: true,
rubberband: true,
showNodeSelectionBox: true,
},
snapline: true,
keyboard: true,
clipboard: true,
});
快捷键和事件方法
this.graph.bindKey(["meta+c", "ctrl+c"], () => {
const cells = this.graph.getSelectedCells();
if (cells.length) {
this.graph.copy(cells);
}
return false;
});
this.graph.bindKey(["meta+x", "ctrl+x"], () => {
const cells = this.graph.getSelectedCells();
if (cells.length) {
this.graph.cut(cells);
}
return false;
});
this.graph.bindKey(["meta+v", "ctrl+v"], () => {
if (!this.graph.isClipboardEmpty()) {
const cells = this.graph.paste({ offset: 32 });
this.graph.cleanSelection();
this.graph.select(cells);
}
return false;
});
//undo redo
this.graph.bindKey(["meta+z", "ctrl+z"], () => {
if (this.graph.history.canUndo()) {
this.graph.history.undo();
}
return false;
});
this.graph.bindKey(["meta+shift+z", "ctrl+shift+z"], () => {
if (this.graph.history.canRedo()) {
this.graph.history.redo();
}
return false;
});
// select all
this.graph.bindKey(["meta+shift+a", "ctrl+shift+a"], () => {
const nodes = this.graph.getNodes();
if (nodes) {
this.graph.select(nodes);
}
});
//delete
this.graph.bindKey("backspace", () => {
const cells = this.graph.getSelectedCells();
// console.log(cells.isEdge())
// if (cells.length) {
// graph.removeCells(cells)
// }
});
// zoom
this.graph.bindKey(["ctrl+1", "meta+1"], () => {
const zoom = this.graph.zoom();
if (zoom < 1.5) {
this.graph.zoom(0.1);
}
});
this.graph.bindKey(["ctrl+2", "meta+2"], () => {
const zoom = this.graph.zoom();
if (zoom > 0.5) {
this.graph.zoom(-0.1);
}
});
// 调整节点大小事件
this.graph.on("node:resized", ({ e, x, y, node, view }) => {
this.setClickNode(node); // 这里是将节点保存到变量的方法
});
// 移动节点事件
this.graph.on("node:moved", ({ e, x, y, node, view }) => {});
// 点击背景板
this.graph.on("blank:click", ({ e, x, y }) => {});
// 点击节点
this.graph.on("node:click", ({ e, x, y, node, view }) => {
this.setClickNode(node);
this.drawer = true;
});
// 添加边事件, 这里是添加线的删除事件
this.graph.on("edge:added", ({ edge, index, options }) => {
edge.addTools([
{
name: "button-remove", // 添加删除按钮
args: {
distance: "85%",
attrs: {
y: -50,
width: 20,
height: 20,
},
},
},
]);
});
// 拖动新增的事件
this.graph.on("node:added", ({ node }) => {
node.addTools([
{
name: "button-remove",
args: {
distance: "85%",
y: -10, // 删除按钮的位置
x: "100%",
},
},
{
name: "boundary",
args: {
// distance: 20,
padding: 15,
},
},
]);
});
三、节点编辑
//获取点击节点
setClickNode(node) {
this.clickNodeTemp = node; // 指向节点
this.clickNode.customAttrs.clickNodeContent = node.attrs.text.text;
this.clickNode.customAttrs.clickNodeContentSize =
node.attrs.text.fontSize;
this.clickNode.customAttrs.clickNodeTextColor = node.attrs.text.fill;
const propTemp = this.clickNodeTemp.getProp(); // 获取样式信息
this.clickNode.customAttrs.clickNodeBoxSizeWidth = propTemp.size.width;
this.clickNode.customAttrs.clickNodeBoxSizeHeight = propTemp.size.height;
this.clickNode.customAttrs.clickNodeBorderColor = node.attrs.body.stroke;
this.clickNode.customAttrs.clickNodeBgColor = node.attrs.body.fill;
const relativePos = node.position({ relative: true }); // 获取位置信息
this.clickNode.customAttrs.positionx = relativePos.x;
this.clickNode.customAttrs.positiony = relativePos.y;
},
// 加一减一或者颜色修改 addOrSubOrColorTemp 判断是点击加还是减 或者是选择颜色的
addOrSubOrColorClickNodeData(addOrSubOrColorTemp, data) {
if (addOrSubOrColorTemp == "add") {
this.clickNode[data]++;
} else if (addOrSubOrColorTemp == "sub") {
this.clickNode[data]--;
}
(this.clickNode.customAttrs.positionx =
this.clickNode.customAttrs.positionx * 1),
(this.clickNode.customAttrs.positiony =
this.clickNode.customAttrs.positiony * 1);
this.clickNodeTemp.updateAttrs(this.clickNode);
if (this.clickNodeTemp.setAttrs) {
// 用这个方法设置颜色样式
this.clickNodeTemp.setAttrs({
body: {
fill: this.clickNode.customAttrs.clickNodeBgColor,
stroke: this.clickNode.customAttrs.clickNodeBorderColor,
},
label: {
fill: this.clickNode.customAttrs.clickNodeTextColor,
},
text: {
text: this.clickNode.customAttrs.clickNodeContent,
fontSize: this.clickNode.customAttrs.clickNodeContentSize,
},
});
if (addOrSubOrColorTemp && addOrSubOrColorTemp !== "color") {
// 用这个方法修改长宽, 位置
this.clickNodeTemp.setProp({
size: {
width: this.clickNode.customAttrs.clickNodeBoxSizeWidth,
height: this.clickNode.customAttrs.clickNodeBoxSizeHeight,
},
position: {
x: this.clickNode.customAttrs.positionx * 1,
y: this.clickNode.customAttrs.positiony * 1,
},
});
}
}
},
到这需求已经完成了,后续为了展示已存在的流程又补了个初始化加载的方法
var obj = JSON.parse(
localStorage.getItem("graphCacheData") || '{"nodes":[],"edges":[]}'
);
if (obj && (obj.edges.length > 0 || obj.nodes.length > 0)) {
let graphCacheData = JSON.parse(localStorage.getItem("graphCacheData"));
this.graph.fromJSON(graphCacheData);
}