拖拽效果图
拖拽后
布局预览
官方: X6 图编辑引擎 | AntV
安装依赖
# npm
npm install @antv/x6 --save
npm install @antv/x6-plugin-dnd --save
npm install @antv/x6-plugin-export --save
需要引入的代码
import { Graph, Shape } from '@antv/x6';
import { Dnd } from "@antv/x6-plugin-dnd";
页面布局实现 我们设计左右布局
<div class="pannels-drag-view">
<div class="left-tree">
<div style="width: 100%;padding: 0px 10px 10px;">
<div style="border-bottom: 1px solid #FAFAFA;"></div>
<div class="tree-one">
<a-space>
<AppstoreFilled height="20px"/>
<span>00000001</span>
</a-space>
</div>
<div style="padding-left: 10px;" class="move-view"
@mousedown="startDrag($event, {key: value.key, title: value.title})">
1111111111111111</div>
<div>
</div>
</div>
<div class="right-view" :style="{height: graphHeight+'px', 'min-height': 500, maxHeight: 900}">
<div ref="container" :style="{height: graphHeight+'px'}"></div>
</div>
</div>
<style lang="less">
.pannels-layout-view {
background-color: #FFFFFF;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
.left-tree {
width: 200px;
min-width: 200px;
border: 1px solid #e5e5e5;
margin-right: 20px;
display: flex;
display: -webkit-flex;
flex-direction: column;
.tree-one {
padding: 10px 0px 0px;
cursor: pointer;
display: flex;
flex-direction: row;
justify-items: start;
}
.move-view {
padding: 10px;
cursor: move;
}
.move-view:hover {
background-color: #F7F7F7;
}
.ant-tree-switcher {
width: 10px;
}
.ant-tree {
.ant-tree-node-content-wrapper {
.cursor_move {
cursor: move;
}
}
}
}
.right-view {
display: flex;
flex-direction: column;
flex: 1;
height: 100%;
overflow: auto;
border: 1px solid #e5e5e5;
overflow: hidden;
position: relative;
.layout-setting {
position: absolute;
top: 1px;
right: 1px;
z-index: 10;
padding: 6px 12px;
background: hsla(0, 0%, 100%, .7);
.ant-btn {
border-width: 0;
}
.ant-btn-icon-only {
padding: 1px 0;
border-width: 0;
}
}
}
}
</style>
创建节点
// 创建节点
Graph.registerNode(
'custom-node',
{
inherit: 'rect',
width: 50,
height: 70,
},
true,
);
在onMounted 中设置画布,和初始化内容
// 初始化画布
graph = new Graph({
container: container.value,
autoResize: true,
background: {
color: '#F2F7FA',
},
interacting: ({cell}) => {
if (cell.getData() == undefined || cell.getData().disableMove) {
return { nodeMovable: false }
}
return true;
},
panning: true,
mousewheel: true,
embedding: {
enabled: true,
findParent({node}) {
const bbox = node.getBBox();
return this.getNodes().filter((nodeTemp) => {
const data = nodeTemp.getData();
if (data && data.parent) {
const targetBox = nodeTemp.getBBox();
const targetBBox = bbox.intersectsWithRect(targetBox);
return targetBBox;
}
return false;
})
}
}
});
处理布局 ,在画布上绘制 19 * 20个虚线方框,作为父容器,如下图
代码示例:
onMounted(() => {
let targetPointTemp = [];
let nodesListTemp = []
// 处理布局
for (let i = 0; i < 20; i++) {
for (let j = 0; j < 9; j++) {
let x = i * 60;
let y = j * 80;
// 设置吸附点
targetPointTemp.push({
x: x,
y: y
})
const rectNode = new Shape.Rect({
id: `${i}-${j}`,
shape: 'custom-node',
x: x,
y: y,
width: 50,
height: 70,
zIndex: 9,
// label: `${i}-${j}`,
data: {
parent: true,
disableMove: true,
cpx: i,
cpy: j
},
draggable: false,
attrs: {
body: {
fill: '#F1F4F6',
stroke: "#333333",
strokeWidth: 1,
strokeDasharray: '4, 4'
},
title: {
text: `${i}-${j}`,
fill: '#333333',
verticalAnchor: 'bottom',
fontSize: 12,
refX: 10,
refY: 60,
}
},
markup: [
{
tagName: 'rect',
selector: 'body',
},
{
tagName: 'text',
selector: 'title',
}
],
})
nodesListTemp.push(rectNode);
}
}
graph.zoom(-0.2);
shapeNodesList.value = nodesListTemp;
// 添加节点到画布
graph.addNodes(nodesListTemp);
})
使用DND画布外向画布内拖拽,并吸附,效果如下:
实现:外部向画布拖拽
import { Dnd } from "@antv/x6-plugin-dnd";
// 移动左侧树,配合DND 与Graph 拖拽与监听
const startDrag = (e, data) => {
const {key, title } = data;
const keys = key.split('-');
const invSn = keys[0];
const id = keys[1];
const newNode = graph.createNode({
id: title?title:'',
shape: 'rect',
width: 150,
height: 30,
draggable: true,
data: {
parent: false,
disableMove: false,
id: id,
partSn: title,
invSn: invSn,
partSn: title,
},
attrs: {
label: {
text: title,
fill: '#FFFFFF',
verticalAnchor: 'middle',
fontSize: 12,
ellipsis: true,
breakWord: true,
textWrap: {
width: -10,
height: -10,
ellipsis: true
}
},
body: {
stroke: "#333333",
strokeWidth: 1,
fill: '#999999'
}
},
zIndex: 11
});
const dnd = new Dnd({
target: graph,
getDragNode: (node) => node.clone({keepId: true}),
getDropNode: (node) => node.clone({keepId: true}),
validateNode: () => {
console.log("drag successed")
},
})
dnd.start(newNode, e);
currentParent.value = null;
}
画布中监听并处理拖拽事件并吸附
onMounted(() => {
// 添加节点监听
graph.on('node:added', (env) => {
const { cell, node } = env;
console.log("node:added");
const data = cell.data;
// 获取父节点
const parent = node.getParent();
let position = cell.position();
if (parent) {
position = parent.getPosition();
if (position.x > maxX.value || position.y > maxY.value || position.x < 0 || position.y < 0) {
graph.removeNode(cell);
return false;
}
} else {
if (position.x > maxX.value || position.y > maxY.value || position.x < 0 || position.y < 0) {
graph.removeNode(cell);
return false;
}
}
// 删除dom
removeDomResData(data, cell);
node.setProp('size', { width: 50, height: 70 });
if (parent) {
// 判断子节点数量
const childCount = parent.getChildCount();
if (childCount > 1) {
startDragOut(data, cell);
return false
}
position = parent.getPosition();
cell.position(position.x, position.y, cell);
cell.setAttrs({
body: {
stroke: "#222222",
strokeWidth: 1,
fill: '#3E82FF'
}
})
cell.setParent(parent);
cell.insertTo(parent);
} else {
const cellParent = cell.getParent();
cell.setAttrs({
body: {
stroke: "#222222",
strokeWidth: 1,
fill: '#3E82FF'
}
})
if (cellParent) {
cell.setParent(cellParent);
cell.insertTo(cellParent);
}
}
saveLayoutData(data, cell);
});
})
嵌入父节点的监听
// 嵌入父节点监听
graph.on('node:embedded', ({ cell, node }) => {
const parent = cell.getParent();
const position = parent.getPosition();
const data = cell.getData(); // 获取节点数据
if (data.parent != undefined && data.parent) {
parent.removeChild(node);
return false;
}
if (position.x > maxX.value || position.y > maxY.value || position.x < 0 || position.y < 0) {
parent.removeChild(node);
cell.position(startX.value, startY.value, cell);
cell.setParent(currentParent.value);
cell.insertTo(currentParent.value);
return false;
}
const childCount = parent.getChildCount();
if (childCount > 1) {
parent.removeChild(node);
if (currentParent.value) {
cell.position(startX.value, startY.value, cell);
cell.setParent(currentParent.value);
cell.insertTo(currentParent.value);
return false
} else {
const cellParent = cell.parent;
if (cellParent) {
const cellCount = cell.parent.getChildCount();
if (cellCount > 1) {
return false
}
const px = cellParent.getPosition().x;
const py = cellParent.getPosition().y;
cell.position(px, py, cell);
cell.setParent(cellParent);
cell.insertTo(cellParent);
return false
}
// cell.setParent(null);
graph.removeCell(cell);
startDragOut(data, cell);
return false;
}
}
cell.position(position.x, position.y, cell);
cell.setAttrs({
body: {
stroke: "#222222",
strokeWidth: 1,
fill: '#3E82FF'
}
})
cell.setParent(parent);
cell.insertTo(parent);
saveLayoutData(data, cell);
});
画布中 子节点的移动处理
onMounted(() => {
// 鼠标按下事件
graph.on('node:mousedown', (node)=>{
console.log("node:mousedown")
const { cell } = node;
const parent = cell.getParent();
if (parent) {
currentParent.value = parent;
} else {
currentParent.value = null;
}
// 记录初始位置
if (!cell.data.parent) {
const position = cell.position();
startX.value = position.x;
startY.value = position.y;
} else {
startX.value = null;
startY.value = null;
}
});
/// 鼠标按下后的离开事件
graph.on("node:mouseup", (env) => {
console.log("node:mouseup")
const { cell, node } = env;
const position = cell.position();
if (position.x < 0 && position.y > 0 && position.y < maxY.value) {
const data = cell.getData();
startDragOut(data, cell);
return false;
}
if (position.x > maxX.value || position.y > maxY.value || position.x < 0 || position.y < 0) {
cell.position(startX.value, startY.value, cell);
if (currentParent.value) {
cell.setParent(currentParent.value);
cell.insertTo(currentParent.value);
}
return false;
}
});
// 监听节点事件函数
graph.on('node:removed', (args) => {
// 更新有效节点数据对象
const { cell } = args;
const data = cell.getData();
removeLayoutData(data);
});
})
画布中布局变化记录事件
// 保存布局变化
const saveLayoutData = (data, cell) => {
const { id, partSn, invSn} = data;
let tempList = notSavedDataList.value;
const position = cell.getPosition();
const x = Math.ceil(position.x / 60);
const y = Math.ceil(position.y / 80);
const _index = _.findIndex(tempList, {id: id});
if (_index >= 0) {
const newData = {
id: id,
laidOutX: x,
laidOutY: y,
partSn: partSn,
invSn: invSn
}
tempList[_index] = newData;
} else {
const newData = {
id: id,
laidOutX: x,
laidOutY: y,
partSn: partSn,
invSn: invSn
}
tempList.push(newData);
}
notSavedDataList.value = tempList;
}
需要结合antv/X6的事件监听事件灵活应用
graph.on('cell:click', ({ e, x, y, cell, view }) => {})
graph.on('node:click', ({ e, x, y, node, view }) => {})
graph.on('edge:click', ({ e, x, y, edge, view }) => {})
graph.on('blank:click', ({ e, x, y }) => {})
graph.on('cell:mouseenter', ({ e, cell, view }) => {})
graph.on('node:mouseenter', ({ e, node, view }) => {})
graph.on('edge:mouseenter', ({ e, edge, view }) => {})
graph.on('graph:mouseenter', ({ e }) => {})