1.简介
X6 是基于 HTML 和 SVG 的图编辑引擎,提供低成本的定制能力和开箱即用的内置扩展,方便我们快速搭建 DAG 图、ER 图、流程图、血缘图等应用。官方文档链接:https://x6.antv.antgroup.com/
2.快速上手并实现一些基本的功能
安装
# npm
$ npm install @antv/x6 --save# yarn
$ yarn add @antv/x6
需要完成的效果大概如下图。1.点击分辨率切换画布的尺寸。2.点击预览就渲染一张全屏的图片,只能看不能修改。3.点保存生成JSON提交给后端然后在其它端使用。4.toptool里面有撤销跟重做功能。5.拖动侧边栏里面的元素生成不同的节点渲染到画布上。6.对节点做一些操作,例如:拖拽移动节点,伸缩旋转节点,右键节点出现菜单可以删除节点,修改颜色之类的。
3.开搞
需求1,创建画布并响应上面选择框的尺寸改变
graph.value = new Graph({
container: document.getElementById("graph"), // 在这个id为graph的元素上渲染画布
width: 1920,
height: 1080,
embedding: {
enabled: true, // 将一个节点拖动到另一个节点成为一个组合
},
grid: {
type: "mesh",
size: 8, // 网格大小,即每次移动元素至少8px
visible: true, // 渲染网格背景
},
});
画布创建好之后,点击下拉选择框改变分辨率,触发change事件并触发画布的大小改变
// 分辨率的枚举
const pixelOptions = [
{ label: "2560*1440", value: 0 },
{ label: "1920*1080", value: 1 },
{ label: "1680*1050", value: 2 },
{ label: "1366*768", value: 3 },
{ label: "1280*960", value: 4 },
{ label: "800*600", value: 5 },
];
// 画布分辨率改变的事件
const graphSizeChange = (val) => {
// 假如val==2 得到一个数组[1680,1050]
let newPixel = pixelOptions.find((i) => i.value == val).label.split("*");
// 调用这个api重新设置画布的尺寸
graph.value.resize(newPixel[0], newPixel[1]);
};
这样就完成了第一个需求。
2.拖动侧边栏里面的元素生成不同的节点渲染到画布上,并且设置机器编号
这个需求使用的x6的一个叫做Dnd的插件
$ yarn add @antv/x6-plugin-dnd
/* template */
<div class="sidebar-item" v-for="item in sidebarOptions" @mousedown="(e) => startDrag(e, item)" :key="item.value" :ref="item.name">
<div class="sidebar-item-img">
<el-image :draggable="false" style="width: 40px" :src="item.src"></el-image>
</div>
<div class="sidebar-item-text">{{ item.text }}</div>
</div>
/* script */
import { Dnd } from '@antv/x6-plugin-dnd'
// 渲染上面页面结构需要的数据
const sidebarOptions = [
{
src: "https://xxxxxxx/computer.png",
text: "电脑",
value: 0,
name: "computer",
},
{
src: "https://xxxxxxx/cashier.png",
text: "收银台",
value: 1,
name: "cashier",
}
];
// 初始化Dnd
dnd.value = new Dnd({
target: graph.value, // 目标画布,就是我们刚刚创建好的那个画布
scaled: false, // 是否缩放所拖动的节点
});
// 拖拽元素所触发的事件
const startDrag = (e, row) => {
if (row.name == "computer") {
// 创建一个自定义的节点
let node = graph.value.createNode({
width: 70,
height: 65,
label: "客户机", // 节点显示的名字
zIndex:999, // 需要保证这个元素在其它元素之上
data: {
type: "computer", // 这里自定义了一个数据,用来区别所拖动的是哪个元素
},
// 使用svg创建节点
markup: [
{
tagName: "path", // 需要渲染的标签名
selector: "computer", // 选择器名称,下面可以通过这个选择器修改这个节点的一些属性
attrs: {
d: "m31.000013,53.400026l-25.260876,0c-1.323189"
}
}
],
// 属性
attrs: {
computer: {
fill: "#E0A31A",
}
},
});
// 开始使用dnd
dnd.value.start(node, e);
}
// 节点添加之后做点什么操作
graph.value.on("node:added", ({ cell,node }) => {
let { type } = node.data; // 从前面自定义的数据里面取到这个type
// 如果添加的是computer这个类型的节点的话就做一些操作
if (type == "computer") {
cell.setZIndex(999) // 给它的层级设置为999
// 弹出输入框让用户输机器号,如果不输入则显示默认的客户机
ElMessageBox.prompt("请输入机器号", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
}).then(({ value }) => {
// 设置显示的机器编号
node.label = value;
});
}else if (type == 'rect'){
// 如果type等于rect就给它的层级设置为1,即放在所有节点的最底层
cell.setZIndex(1)
}
});
效果如下图
3.toptool里面的撤销跟重做功能
这个功能是依赖一个官方的x6-plugin-history插件来实现,启用了这个插件之后x6会记录对画布的各种操作。
// 监听这个事件来控制当前画布是否可以撤销,重做
graph.value.on("history:change", () => {
state.canBeRedo = graph.value.canRedo();
state.canBeUndo = graph.value.canUndo();
});
// 处理重做的事件
const handleRedo = () => {
graph.value.redo();
};
// 处理撤销的事件
const handleUndo = () => {
graph.value.undo();
};
4.右键节点出现菜单可以删除节点,修改颜色之类的。
要实现菜单的思路如下:1.节点的右键事件里面获取到x,y坐标,根据x,y渲染菜单。2.其它的任何操作都关闭掉菜单(移动,拖拽节点,点击了任何除菜单的区域),这里可以考虑在菜单出现的时候全局监听鼠标的左键按下事件并关闭菜单
/* template */
/* 使用Teleport将这个元素层级提升到body里面,这样就可以使用fixed来定位元素 */
<Teleport to="body">
<div id="myContextMenu" v-if="contextMenuVisible" class="contextmenu">
<div class="contextmenu-item" data-type="menuItem" @click="handleNodeContextMenu(0)">删除节点</div>
<div class="contextmenu-item" data-type="menuItem" @click="handleNodeContextMenu(1)">修改节点样式</div>
<div class="contextmenu-item" data-type="menuItem" @click="handleNodeContextMenu(2)">置于顶层</div>
<div class="contextmenu-item" data-type="menuItem" @click="handleNodeContextMenu(3)">置于底层</div>
</div>
</Teleport>
/* script */
const contextMenuVisible = ref(false)
// 节点右键事件
graph.value.on("node:contextmenu", ({ e, node }) => {
let { type } = node.data;
rightClickCell.value = node; // 用一个变量记录下当前被右键的节点
contextMenuVisible.value = true; // 显示菜单
nextTick(() => {
let dom = document.getElementById("myContextMenu"); // 获取菜单的dom
dom.style.top = e.clientY + "px"; // 设置菜单位置
dom.style.left = e.clientX + "px";
});
});
// 处理各个菜单项的点击事件
const handleNodeContextMenu = (type) => {
type === 0 && graph.value.removeNode(rightClickCell.value); // 调用api移除节点
type === 1 && nodeColorEditRef.value && nodeColorEditRef.value.open(); // 打开修改颜色的弹框
type === 2 && rightClickCell.value.toFront(); // 调用api置顶该节点
type === 3 && rightClickCell.value.toBack(); // 调用api置底该节点
contextMenuVisible.value = false; // 操作完成之后手动关闭菜单
};
// 监听右键菜单当前是否显示来控制是否有这个关闭的事件监听器
watch(contextMenuVisible, (val) => {
if (val) {
document.addEventListener("mousedown", closeContextMenu);
} else {
document.removeEventListener("mousedown", closeContextMenu);
}
});
// 关闭菜单的事件,因为劫持了整个屏幕的mousedown事件,所以使用自定义属性来正确触发菜单的点击事件
const closeContextMenu = (e) => {
let type = e.target.getAttribute("data-type");
// 如果type没有值或者不等于menuItem则说明点击的不是右键菜单项
if (!type || type !== "menuItem") {
contextMenuVisible.value = false;
}
};
这样就完成了右键菜单的需求了,效果如下图
5.预览以及导出
预览的话思路是将这个画布导出为图片进行渲染(使用x6-plugin-export这个插件),保存则是将画布生成一个JSON提交后端
# yarn add @antv/x6-plugin-export
// 预览
graph.toPNG((image)=>{
console.log(image); // 生成了一个base64的图片,渲染它就好了
})
graph.toJSON() // 将画布内容转成JSON就是保存