因为是把已经他当作组件来使用,所以大家使用的时候有些需要改一下,大部分的功能都在里面了,希望可以帮到大家。目前有时候使用会出现部分bug,vue-contextmenujs这个组件是用来实现右击显示菜单的,我也是网上找的,效果还可以。
参考:VUE右键菜单 vue-contextmenujs的使用_~Serendipity~的博客-优快云博客
<template>
<div>
<el-row>
<el-button type="primary" @click="toSvg()">导出SVG</el-button>
<el-button type="success" @click="toJpg()">导出PNG</el-button>
<el-button type="danger" @click="graphSave()">保存</el-button>
</el-row>
<!-- 创建容器 -->
<div ref="container" id="container" style="margin-top: 20px;"></div>
</div>
</template>
<script>
// 准备数据
import { Graph } from '@antv/x6'
import { Export } from '@antv/x6-plugin-export'
import { Transform } from '@antv/x6-plugin-transform'
import { Snapline } from '@antv/x6-plugin-snapline'
import { Clipboard } from '@antv/x6-plugin-clipboard'
import { Keyboard } from '@antv/x6-plugin-keyboard'
import { Selection } from '@antv/x6-plugin-selection'
import { History } from '@antv/x6-plugin-history'
import { Dnd } from '@antv/x6-plugin-dnd'
export default {
/**
* 待优化:
* 1.添加节点按钮
* 3.多个画布之间调换,确认是否保存内容
* 6.数据的流通,保存到库
* 7.树的按钮不显示
*/
data() {
return {
graph: null,
dnd: null,
data: {
// 初始化展示的节点和边
// nodes: [
// {
// id: 'node1', // String,可选,节点的唯一标识
// shape: 'custom-node',
// x: 40, // Number,必选,节点位置的 x 值
// y: 40, // Number,必选,节点位置的 y 值
// width: 100, // Number,可选,节点大小的 width 值
// height: 40, // Number,可选,节点大小的 height 值
// label: 'hello', // String,节点标签
// ports: {
// groups: {
// top: {
// position: 'top',
// attrs: {
// circle: {
// magnet: true,
// stroke: 'black',
// r: 5,
// },
// },
// },
// bottom: {
// position: 'bottom',
// attrs: {
// circle: {
// magnet: true,
// stroke: 'black',
// r: 5,
// },
// },
// },
// left: {
// position: 'left',
// attrs: {
// circle: {
// magnet: true,
// stroke: 'black',
// r: 5,
// },
// },
// },
// right: {
// position: 'right',
// attrs: {
// circle: {
// magnet: true,
// stroke: 'black',
// r: 5,
// },
// },
// },
// },
// items: [
// {
// id: 'node1_port_1',
// group: 'bottom',
// },{
// id: 'node1_port_2',
// group: 'top',
// },{
// id: 'node1_port_3',
// group: 'left',
// },{
// id: 'node1_port_4',
// group: 'right',
// }
// ],
// },
// },
// {
// id: 'node2',
// shape: 'custom-node',
// x: 160,
// y: 180,
// width: 100,
// height: 40,
// label: 'world',
// ports: {
// groups: {
// top: {
// position: 'top',
// attrs: {
// circle: {
// magnet: true,
// stroke: 'black',
// r: 5,
// },
// },
// },
// bottom: {
// position: 'bottom',
// attrs: {
// circle: {
// magnet: true,
// stroke: 'black',
// r: 5,
// },
// },
// },
// left: {
// position: 'left',
// attrs: {
// circle: {
// magnet: true,
// stroke: 'black',
// r: 5,
// },
// },
// },
// right: {
// position: 'right',
// attrs: {
// circle: {
// magnet: true,
// stroke: 'black',
// r: 5,
// },
// },
// },
// },
// items: [
// {
// id: 'node2_port_1',
// group: 'bottom',
// },{
// id: 'node2_port_2',
// group: 'top',
// },{
// id: 'node2_port_3',
// group: 'left',
// },{
// id: 'node2_port_4',
// group: 'right',
// }
// ],
// },
// }
// ],
// edge: {
// source: 'node1',
// target: 'node2',
// router: 'orth',
// attrs: {
// line: {
// stroke: '#8f8f8f',
// strokeWidth: 1,
// },
// },
// }
},
saveData: [],
saveId: '',
saveVal:'',
canRedo: false, // 恢复
canUndo: false, // 撤销
}
},
created() {},
mounted() {
this.initGraph()
},
methods: {
// 初始化画布
initGraph () {
// 渲染画布
this.graph = new Graph({
container: this.$refs.container,
// 画布背景
background: {
color: '#F2F7FA',
},
// 画布的线条样式
grid: {
visible: true, // 渲染网格背景
size: 15, // 网格大小 10px
},
width: 600,
height: 495,
// 画布平移
panning: {
enabled: true,
// modifiers:拖拽可能和其他操作冲突,,
// 设置修饰键后需要按下修饰键并点击鼠标才能触发画布拖拽。
modifiers: 'Ctrl',
// eventTypes:设置触发画布拖拽的行为,支持 leftMouseDown、 rightMouseDown、mouseWheel,
// 默认为 ['leftMouseDown']
eventTypes: ['leftMouseDown']
},
mousewheel: { // 放大
enabled: true,
modifiers: 'Ctrl',
maxScale: 4,
minScale: 0.2,
},
// 连接线
connecting: {
allowBlank: false, // 是否允许连接到画布空白位置的点
allowLoop: false, // 是否允许创建循环连线,即边的起始节点和终止节点为同一节点
allowNode: true, // 是否允许边连接到节点(非节点上的连接桩)
allowEdge: false, // 是否允许边连接到另一个边
allowPort: true, // 是否允许边连接到连接桩
allowMulti: true, //是否允许在相同的起始节点和终止之间创建多条边
highlight: true,
createEdge() {
return this.createEdge({
shape: 'edge',
router: {
name: 'manhattan',
args: {
startDirections: ['top', 'right', 'bottom', 'left'],
endDirections: ['top', 'right', 'bottom', 'left'],
},
},
connector: {
name: 'rounded',
// args: { radius: 10, },
},
attrs: {
line: {
stroke: '#8f8f8f',
strokeWidth: 2,
},
},
tools: ['edge-editor'], // 文本编辑器
})
},
},
// 连接桩样式
highlighting: {
// 连接桩吸附连线时在连接桩外围围渲染一个包围框
magnetAdsorbed: {
name: 'stroke',
args: {
attrs: {
fill: '#8f8f8f',
fillOpacity: '0.3',
stroke: '#fff',
strokeOpacity: '0'
},
},
},
},
// interacting: {
// nodeMovable: true, //节点是否可以移动
// edgeMovable: true, //边是否可以被移动
// },
})
// 自定义节点
Graph.registerNode('custom-node',
{
inherit: 'rect', // 继承于 rect 节点
markup: [
{
tagName: 'rect', // 标签名称
selector: 'body', // 选择器
},
{
tagName: 'image',
selector: 'img',
},
{
tagName: 'text',
selector: 'label',
},
],
attrs: {
body: {
stroke: '#8f8f8f',
strokeWidth: 1,
fill: '#fff',
rx: 6,
ry: 6
},
// img: {
// 'xlink:href': 'https://gw.alipayobjects.com/zos/antfincdn/FLrTNDvlna/antv.png',
// width: 16,
// height: 16,
// x: 12,
// y: 12,
// },
},
ports: {
groups: {
top: {
position: 'top',
attrs: {
circle: {
magnet: true,
stroke: 'black',
r: 4,
},
},
},
bottom: {
position: 'bottom',
attrs: {
circle: {
magnet: true,
stroke: 'black',
r: 4,
},
},
},
left: {
position: 'left',
attrs: {
circle: {
magnet: true,
stroke: 'black',
r: 4,
},
},
},
right: {
position: 'right',
attrs: {
circle: {
magnet: true,
stroke: 'black',
r: 4,
},
},
},
},
items: [
{
id: 'port_1',
group: 'bottom',
},{
id: 'port_2',
group: 'top',
},{
id: 'port_3',
group: 'left',
},{
id: 'port_4',
group: 'right',
}
],
},
},true)
// 图形变换
this.graph.use(
new Transform({
// 调整大小
resizing: {
enabled: true
},
// 旋转角度
rotating: {
enabled: true
}
}),
)
// 导出
this.graph.use(new Export())
// 框选
this.graph.use(
new Selection({
enabled: true,
multiple: true,
rubberband: true,
movable: true,
showNodeSelectionBox: true,
showEdgeSelectionBox: true,
pointerEvents: 'none'
}),
)
// 快捷键
this.graph.use(
new Keyboard({
enabled: true
}),
)
// 复制粘贴
this.graph.use(
new Clipboard({
enabled: true,
global: false //是否为全局键盘事件
}),
)
//撤销重做
this.graph.use(
new History({
enabled: true,
}),
)
// 对齐线
this.graph.use(
new Snapline({
enabled: true,
clean: false, // 如果为 true,则在 3s 后清除对齐线,为 false,不会清除,如果为数字(ms),则在指定时间后清除对齐线
}),
)
// Dnd
this.dnd = new Dnd({
target: this.graph,
scaled: false,
// dndContainer: container
})
this.graph.fromJSON(this.data)
// 双击节点
this.graph.on('cell:dblclick', ({ e, x, y, cell, view }) => {
// this.$emit(dbclickAlert,true)
// this.$parent.dbclickAlert(true)
if(this.graph.isNode(cell)){
console.log('data',cell.getData())
this.$parent.$parent.$parent.dbclickAlert(cell,true)
}
})
// 历史改变
this.graph.on('history:change', () => {
this.canRedo = this.graph.canRedo(),
this.canUndo = this.graph.canUndo()
})
// 右键菜单
this.graph.on('node:contextmenu',({ e, x, y, cell, view }) => {
this.onContextmenu(e)
})
this.graph.on('edge:contextmenu',({ e, x, y, cell, view }) => {
this.onContextmenu(e)
})
this.graph.on('blank:contextmenu',({ e, x, y, cell, view }) => {
this.onContextmenu(e)
})
this.graph.on('node:mouseenter', () => {
const container = document.getElementById('container')
const ports = container.querySelectorAll(
'.x6-port-body',
)
this.showPorts(ports, true)
})
this.graph.on('node:mouseleave', () => {
const container = document.getElementById('container')
const ports = container.querySelectorAll(
'.x6-port-body',
)
this.showPorts(ports, false)
})
// 复制
this.graph.bindKey('ctrl+c', () => {
const cells = this.graph.getSelectedCells()
if (cells.length) {
this.graph.copy(cells)
}
return false
})
// 粘贴
this.graph.bindKey('ctrl+v', () => {
if (!this.graph.isClipboardEmpty()) {
const cells = this.graph.paste({ offset: 32 })
this.graph.cleanSelection()
this.graph.select(cells)
}
return false
})
// 撤销
this.graph.bindKey('ctrl+z', () => {
this.graph.undo()
})
// 恢复
this.graph.bindKey('ctrl+y', () => {
this.graph.redo()
})
// 剪切
this.graph.bindKey('ctrl+x', () => {
const cells = this.graph.getSelectedCells()
if (cells.length) {
this.graph.copy(cells)
this.graph.removeCells(cells)
}
})
// 删除
this.graph.bindKey('Backspace', () => {
const cells = this.graph.getSelectedCells()
if (cells.length) {
this.graph.removeCells(cells)
}
return false
})
},
// 导出svg
toSvg () {
this.graph.exportSVG('导出svg格式')
},
// 导出png
toJpg () {
this.graph.exportPNG('导出png格式')
},
// 销毁画布
graphDispose () {
this.graph.dispose()
},
// 将画布中元素居中展示
graphCenter(){
this.graph.centerContent()
},
// 添加节点
graphAddNode () {
this.graph.addNode({
shape: 'custom-node',
x: 10,
y: 10,
width: 100,
height: 40,
label: 'hello',
})
},
// 修改节点
graphUpdate () {
const nodes = this.graph.getNodes()
nodes.forEach((node) => {
const width = 100 + Math.floor(Math.random() * 50)
const height = 40 + Math.floor(Math.random() * 10)
node.prop('size', { width, height })
const color = this.color16()
node.attr('body/fill', color)
})
},
// 十六进制颜色随机
color16 () {
var r = Math.floor(Math.random() * 256);
var g = Math.floor(Math.random() * 256);
var b = Math.floor(Math.random() * 256);
var color = "#" + r.toString(16) + g.toString(16) + b.toString(16);
return color;
},
// 保存
graphSave () {
this.saveData = this.graph.toJSON()
const saveDataTrans = JSON.stringify(this.saveData)
// 存到库中
this.$http({
url: this.$http.adornUrl('/basedata/transrelation/update'),
method: 'post',
data: this.$http.adornData({
'id': this.saveId,
'chartCode': saveDataTrans
})
}).then(({data}) => {
if (data && data.code === 0) {
this.$emit('reassign',this.saveVal)
this.$message({
message: '保存成功',
type: 'success'
})
} else {
this.$message.error(data.msg)
}
})
},
// 重新渲染
graphRecreate (val) {
if(val === undefined || val === null || val === ''){
this.initGraph()
}else{
this.saveData = JSON.parse(val.chartCode)
this.saveId = val.id
this.saveVal = val
this.graph.fromJSON(this.saveData)
this.$message({
message: '渲染成功',
type: 'success'
})
}
},
// 撤销
undo () {
this.graph.undo()
},
// 恢复
redo (){
this.graph.redo()
},
// Dnd拖拽
startDrag (node, e) {
console.log(node, e)
const node1 = this.graph.createNode({
shape: 'custom-node',
width: 120,
height: 50,
label: node.data.name+"\n"+"(0%~0%)",
data: {
minNum: 0,
maxNum: 0
}
})
this.dnd.start(node1, e)
},
// 右击菜单事件
onContextmenu (event) {
const cells = this.graph.getSelectedCells()
const isRedo = this.canRedo
const isUndo = this.canUndo
this.$contextmenu({
items: [
{
label: "复制(Ctrl+Z)",
disabled: cells.length===0?true:false,
onClick: () => {
if (cells.length) {
this.graph.copy(cells)
}
}
},
{
label: "粘贴(Ctrl+V)",
disabled: this.graph.isClipboardEmpty(),
onClick: () => {
if (!this.graph.isClipboardEmpty()) {
const cells = this.graph.paste({ offset: 32 })
this.graph.cleanSelection()
this.graph.select(cells)
}
}
},
{
label: "剪切(Ctrl+X)",
divided: true,
disabled: cells.length===0?true:false,
onClick: () => {
if (cells.length) {
this.graph.copy(cells)
this.graph.removeCells(cells)
}
}
},
{
label: "撤销(Ctrl+Z)",
disabled: !isUndo,
onClick: () => {
this.undo()
}
},
{
label: "恢复(Ctrl+Y)",
disabled: !isRedo,
divided: true,
onClick: () => {
this.redo()
}
},
{
label: "删除(Backspace)",
disabled: cells.length===0?true:false,
onClick: () => {
if (cells.length) {
this.graph.removeCells(cells)
}
}
}
],
event, // 鼠标事件信息
customClass: "custom-class", // 自定义菜单 class
zIndex: 100, // 菜单样式 z-index
minWidth: 150 // 主菜单最小宽度
});
return false;
},
// 弹框赋值
updateByAlert (min, max, cell) {
if(min===""){
min=0
}
if(max===""){
max=0
}
cell.setData({minNum:min,maxNum:max},{ overwrite: true })
cell.label = cell.label.substr(0,cell.label.lastIndexOf("\n"))+"\n("+min+"%~"+max+"%)"
},
// 控制连接桩显示/隐藏
showPorts (ports, show) {
for (let i = 0, len = ports.length; i < len; i = i + 1) {
ports[i].style.visibility = show ? 'visible' : 'hidden'
}
},
}
}
</script>
<style scoped>
</style>
--------------------------2023.10.31-------------------------
1. 在实际使用中我发现了一些bug,在点击保存时会调用graphRecreate重新渲染,之后如果你单击或右击任意节点,可能会出现没有按住鼠标但节点会自己移动,并且框选的框子会与节点分离的情况,也有点击其他页面再点回来画布不会自动伸缩resize,这时候可以在graphRecreate方法里先使用this.graph.dispose()来清空画布,this.initGraph()重新渲染即可。
2. 使用时发现缩放页面,画布不会自动伸缩,需要在this.initGraph()方法中定义画布时加入autoResize: true即可 ,this.graph = new Graph({ autoResize: true ])
-------------------------2023.11.30---------------------------
需求:获取从根节点到叶子节点的所有路径(前提是没有形成环)
//this.allNodes保存了所有路径
animate() {
// this.graph.getSuccessors(element)
// this.graph.getLeafNodes()
//获取所有根节点(没有入边的节点)
var roots = this.graph.getRootNodes();
roots.forEach(element => {
this.stack.push(element);
this.getNeighbor(element);
this.stack.pop();
});
}
//递归获取子节点
getNeighbor(cell) {
//获取该节点的所有相邻节点(出边所指的节点)
var neighbors = this.graph.getNeighbors(cell, { outgoing: true });
if (neighbors.length == 0) {
var s = [];
this.stack.forEach(element1 => {
s.push(element1);
});
this.allNodes.push(s);
}
neighbors.forEach(element => {
this.stack.push(element);
this.getNeighbor(element);
this.stack.pop();
});
},
-------------------------2024.01.22---------------------------
左边的树是使用了elementui的树组件,代码如下:
<el-tree
:data="menus"
:expand-on-click-node="expandOnClickNode === undefined ? false : expandOnClickNode"
:default-expanded-keys="defaultExpendKey"
:highlight-current="true"
:current-node-key="defaultKey"
ref="tree2"
node-key="id"
:show-checkbox="showCheckbox === undefined ? false : showCheckbox"
:props="defaultProps"
@node-click="handleNodeClick"
@check="handleCheckChange"
:default-checked-keys="multipleCheckData"
:filter-node-method="filterNode"
:draggable="draggable === undefined ? false : draggable"
@node-drag-start="startDrag"
>
//拖拽方法,回调node和event参数,传给Dnd拖拽方法
startDrag (node, event) {
this.$emit('startDrag', node, event)
},