拖拽效果图
拖拽后
布局预览
官方: 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 }) => {})