流程图,支持字体图标,颜色,可连接线,点击时右侧展示相关的详细信息
调研了多种可拖拽流程图的技术,如:bpmn.js,gojs等,由于bpmn-js功能冗余,GOJS相对于更加轻量级,最终选用GOJS开发此功能
1.安装
npm install gojs --save
2.在main.js中引入
import gojs from ‘gojs’
Vue.prototype.go = gojs
使用TextBlock类显示文本。
设置TextBlock.text属性是显示文本字符串的唯一方法。因为TextBlock继承自GraphObject,所以某些GraphObject属性会影响文本
文本的大小和样式外观由TextBlock.font指定。该值可以是任何CSS字体说明符字符串。
myDiagram.nodeTemplateMap.add('Pending',
$(go.Node, 'Spot', this.nodeStyle(),
$(go.Panel, 'Auto',
$(go.Shape, 'RoundedRectangle',
{ width: 100,height: 40, fill: '#17c2b9', stroke: null, portId: "", //设置统一的宽高 the default port: if no spot on link data, use closest side
fromLinkable: true, toLinkable: true, cursor: "pointer", },
new go.Binding("location", "loc", go.Point.parse)),
$(go.Panel, "Horizontal", { margin: 5 },
$(go.TextBlock,"Pending", { text: '\uf030', font: '10pt FontAwesome' ,textAlign:'left',verticalAlignment: go.Spot.Left,}),
$(go.TextBlock,"Pending",
{
textAlign:'right',
font: 'bold 11pt Helvetica, Arial, sans-serif',
stroke: '#fff',
margin:5,
maxSize: new go.Size(100, NaN),
wrap: go.TextBlock.WrapFit,
editable: true
},
new go.Binding('text'))
),
),
))
首先,在创建图表之前,请确保该字体已加载到页面中,在main.js中引入
import './assets/fontAwesome/css/font-awesome.min.css'
$(go.TextBlock,"Pending",{ text: '\uf030', font: '10pt FontAwesome',}),
许多链接确实希望通过使用箭头来指示方向性。 GoJS使创建通用箭头变得容易:只需添加Shape并设置其Shape.toArrow属性即可。设置该属性将自动分配一个几何到Shape.geometry 并且使得箭头位于连杆的头部并以正确的方向指向将设置其他属性。
diagram.nodeTemplate =
$(go.Node, "Auto",
new go.Binding("location", "loc", go.Point.parse),
$(go.Shape, "RoundedRectangle", { fill: "lightgray" }),
$(go.TextBlock, { margin: 5 },
new go.Binding("text", "key"))
);
diagram.linkTemplate =
$(go.Link,
$(go.Shape), // the link shape
$(go.Shape, // the arrowhead
{ toArrow: "OpenTriangle", fill: null })
);
var nodeDataArray = [
{ key: "Alpha", loc: "0 0" },
{ key: "Beta", loc: "100 50" }
];
var linkDataArray = [
{ from: "Alpha", to: "Beta" }
];
diagram.model = new go.GraphLinksModel(nodeDataArray, linkDataArray);
makePort (name, spot, output, input) {
return $(go.Shape, 'Circle',
{
fill: 'transparent',
stroke: null, // this is changed to 'white' in the showPorts function
desiredSize: new go.Size(8, 8),
alignment: spot,
alignmentFocus: spot, // align the port on the main Shape
portId: name, // declare this object to be a 'port'
fromSpot: spot,
toSpot: spot, // declare where links may connect at this port
fromLinkable: output,
toLinkable: input, // declare whether the user may draw links to/from here
cursor: 'pointer' // show a different cursor to indicate potential link point
})
},
四个命名端口,每侧一个:参数output是输出,input是输入
this.makePort('T', go.Spot.Top, false, true),
this.makePort('L', go.Spot.Left, true, true),
this.makePort('R', go.Spot.Right, true, true),
this.makePort('B', go.Spot.Bottom, true, false),
GoJS提供了几种自动布局,包括:
GridLayout(栅格布局)
TreeLayout(树形布局)
ForceDirectedLayout(力导向布局)
LayeredDigraphLayout(分层有向图布局)
CircularLayout(圆形布局)
当自动布局生效后,用户进行新增节点或者删除节点,会再次触发自动布局效果,原有坐标均被重置.也就是说,只要用户有新增节点的操作,前面的修改会全部重置
解决方案:
将isOngoing设置为false,以防止添加或删除部件等操作使此布局无效。默认值为true。
layout: $(go.TreeLayout,{ isInitial: false, isOngoing: true, angle:90 },),
具体可查看官网地址https://gojs.net/latest/api/symbols/Layout.html
图表模型以JSON格式保存
{ "class": "go.GraphLinksModel",
"nodeDataArray": [
{"category":"Command",
"title":"tsfsfsfsfsfsf",
"text":"源码构建",
"key":-3,
"loc":"-109.79687500000006 -248.24999999999994"
}
],
"linkDataArray":[{"from":-2, "to":-3, "curviness":-20, "points":[ -202.90625,-353.06227569580085,-202.90625,-343.06227569580085,-202.90625, -308.875,-109.796875,-308.875,-109.796875,-274.6877243041992,-109.796875,-264.68772 43041992
]}
]}
使用Link类可实现节点之间的可视关系。 默认情况下会产生一条轻微的曲线。
您可以通过设置Link.curviness属性来控制其弯曲程度。
$(go.Link,
{ curve: go.Link.Bezier,
},
$(go.Shape),
$(go.Shape, { toArrow: "Standard" })
);
防止两个节点之间出现多条连线
nodeStyle () {
return [
// The Node.location comes from the "loc" property of the node data,
// converted by the Point.parse static method.
// If the Node.location is changed, it updates the "loc" property of the node data,
// converting back using the Point.stringify static method.
new go.Binding('location', 'loc', go.Point.parse).makeTwoWay(go.Point.stringify),
{// the Node.location is at the center of each node
locationSpot: go.Spot.Center,
//isShadowed: true,
//shadowColor: "#888",
// handle mouse enter/leave events to show/hide the ports
mouseEnter: (e, obj) => {
this.showPorts(obj.part, true)
},
mouseLeave: (e, obj) => {
this.showPorts(obj.part, false)
}
},
{
linkValidation: function (fromNode, fromPort, toNode, toPort) {
// 防止两个节点之间出现多条连线
return fromNode.findLinksOutOf().all(function (link) {
console.log(link.toNode !== toNode)
return link.toNode !== toNode;
})
},
},
]
},
<template>
<div style='width:100%; white-space:nowrap;'>
<span style='border: 1px solid gray;display: inline-block; vertical-align: top; width:150px;'>
<div ref='myPaletteDiv' style='height: 500px;'>1111</div>
</span>
<span style='border: 1px solid gray;display: inline-block; vertical-align: top; width:40%;'>
<div ref='myDiagramDiv' style='height: 500px'></div>
</span>
</div>
</template>
<script>
let $ = go.GraphObject.make
export default {
name: '',
props: ['modelData'],
data () {
return {
diagram: null
}
},
mounted () {
let self = this
let myDiagram =
$(go.Diagram, this.$refs.myDiagramDiv,
{ // have mouse wheel events zoom in and out instead of scroll up and down/具有鼠标滚轮事件放大和缩小,而不是上下滚动
"toolManager.mouseWheelBehavior": go.ToolManager.WheelZoom,
// initialAutoScale: go.Diagram.Uniform,加上之后定义的出入口就失效了
// "linkingTool.direction": go.LinkingTool.ForwardsOnly,
initialDocumentSpot:go.Spot.Top,
initialContentAlignment: go.Spot.Center,// 居中显示
// layout: $(go.TreeLayout,{ isInitial: false, isOngoing: true, angle:90 },),
'undoManager.isEnabled': true, 支持 Ctrl-Z 和 Ctrl-Y 操作
// Model ChangedEvents get passed up to component users
'ModelChanged': function (e) {
self.$emit('model-changed', e)
},
'ChangedSelection': function (e) {
self.$emit('changed-selection', e)
},
'Modified': function (e) {
self.$emit('modified', e)
},
'TextEdited': function (e) {
self.$emit('text-edited', e)
},
allowDrop: true
})
myDiagram.nodeTemplateMap.add('Start',
$(go.Node, 'Spot', this.nodeStyle(),
$(go.Panel, 'Auto',
$(go.Shape, 'RoundedRectangle',
// 设置统一的宽高
{
width: 100,height: 40, fill: '#98FB98', stroke: null, portId: "", // the default port: if no spot on link data, use closest side
fromLinkable: true, toLinkable: true, cursor: "pointer",
// fromSpot:go.Spot.TopCenter,toSpot:go.Spot.BottomCenter,
},
new go.Binding("location", "loc", go.Point.parse)),
$(go.Panel, "Horizontal",{ margin: 5 },
$(go.TextBlock,"Start",
{ text: '\uf1c1', font: '10pt FontAwesome' ,textAlign:'left',verticalAlignment: go.Spot.Left,}),
$(go.TextBlock,"Start",
{
textAlign:'right',
font: 'bold 11pt Helvetica, Arial, sans-serif',
stroke: '#fff',
margin:5,
maxSize: new go.Size(100, NaN),
wrap: go.TextBlock.WrapFit,
editable: false
},
new go.Binding('text'))
),
),
// three named ports, one on each side except the top, all output only
this.makePort('L', go.Spot.Left, true, false),
this.makePort('R', go.Spot.Right, true, false),
this.makePort('B', go.Spot.Bottom, true, false),
))
myDiagram.nodeTemplateMap.add('Pending',
$(go.Node, 'Spot', this.nodeStyle(),
$(go.Panel, 'Auto',
$(go.Shape, 'RoundedRectangle',
{ width: 100,height: 40, fill: '#17c2b9', stroke: null, portId: "", //设置统一的宽高 the default port: if no spot on link data, use closest side
fromLinkable: true, toLinkable: true, cursor: "pointer", },
new go.Binding("location", "loc", go.Point.parse)),
$(go.Panel, "Horizontal", { margin: 5 },
$(go.TextBlock,"Pending", { text: '\uf030', font: '10pt FontAwesome' ,textAlign:'left',verticalAlignment: go.Spot.Left,}),
$(go.TextBlock,"Pending",
{
textAlign:'right',
font: 'bold 11pt Helvetica, Arial, sans-serif',
stroke: '#fff',
margin:5,
maxSize: new go.Size(100, NaN),
wrap: go.TextBlock.WrapFit,
editable: true
},
new go.Binding('text'))
),
),
// four named ports, one on each side:
this.makePort('T', go.Spot.Top, false, true),
this.makePort('L', go.Spot.Left, true, true),
this.makePort('R', go.Spot.Right, true, true),
this.makePort('B', go.Spot.Bottom, true, false),
))
myDiagram.nodeTemplateMap.add('End',
$(go.Node, 'Spot', this.nodeStyle(),
$(go.Panel, 'Auto',
$(go.Shape, 'RoundedRectangle',
// 设置统一的宽高
{
width: 100,height: 40, fill: '#8e9499', stroke: null, portId: "", // the default port: if no spot on link data, use closest side
fromLinkable: true, toLinkable: true, cursor: "pointer",
},
new go.Binding("location", "loc", go.Point.parse)),
$(go.Panel, "Horizontal",
{ margin: 5 },
$(go.TextBlock,"End",
{ text: '\uf039', font: '10pt FontAwesome' ,textAlign:'left',verticalAlignment: go.Spot.Left,}),
$(go.TextBlock,"End",
{
textAlign:'right',
font: 'bold 11pt Helvetica, Arial, sans-serif',
stroke: '#fff',
margin:5,
maxSize: new go.Size(100, NaN),
wrap: go.TextBlock.WrapFit,
editable: false
},
new go.Binding('text'))
),
),
// three named ports, one on each side except the bottom, all input only:
this.makePort('T', go.Spot.Top, false, true),
this.makePort('L', go.Spot.Left, false, true),
this.makePort('R', go.Spot.Right, false, true),
))
myDiagram.linkTemplate =
$(go.Link,
$(go.Shape,
new go.Binding("stroke", "color"),
new go.Binding("strokeWidth", "width"),
new go.Binding("strokeDashArray", "dash"))
);
let myPalette =
$(go.Palette, this.$refs.myPaletteDiv, // must name or refer to the DIV HTML element
{
'animationManager.duration': 800, // slightly longer than default (600ms) animation
nodeTemplateMap: myDiagram.nodeTemplateMap, // share the templates used by myDiagram
// nodeTemplate: myDiagram.nodeTemplate, // share the templates used by myDiagram
model: new go.GraphLinksModel([ // specify the contents of the Palette
{"key":0, "category":"Start", "loc":"175 0", "text":"开始"},
{"key":1, "category":"Pending", "loc":"175 50", "text":"源码检查"},
{"key":2, "category":"Pending","loc":"175 100", "text":"源码构建"},
{"key":3, "category":"Pending","loc":"175 450", "text":"自动测试"},
{"key":4, "category":"End", "loc":"175 500", "text":"结束"}
])
})
console.log(myPalette)
this.diagram = myDiagram
this.updateModel(this.modelData)
},
watch: {
modelData: function (val) {
console.log('watch')
console.log(val)
this.updateModel(val)
}
},
computed: {},
methods: {
makePort (name, spot, output, input) {
return $(go.Shape, 'Circle',
{
fill: 'transparent',
stroke: null, // this is changed to 'white' in the showPorts function
desiredSize: new go.Size(8, 8),
alignment: spot,
alignmentFocus: spot, // align the port on the main Shape
portId: name, // declare this object to be a 'port'
fromSpot: spot,
toSpot: spot, // declare where links may connect at this port
fromLinkable: output,
toLinkable: input, // declare whether the user may draw links to/from here
cursor: 'pointer' // show a different cursor to indicate potential link point
})
},
nodeStyle () {
return [
// The Node.location comes from the "loc" property of the node data,
// converted by the Point.parse static method.
// If the Node.location is changed, it updates the "loc" property of the node data,
// converting back using the Point.stringify static method.
new go.Binding('location', 'loc', go.Point.parse).makeTwoWay(go.Point.stringify),
{// the Node.location is at the center of each node
locationSpot: go.Spot.Center,
//isShadowed: true,
//shadowColor: "#888",
// handle mouse enter/leave events to show/hide the ports
mouseEnter: (e, obj) => {
this.showPorts(obj.part, true)
},
mouseLeave: (e, obj) => {
this.showPorts(obj.part, false)
}
},
{
linkValidation: function (fromNode, fromPort, toNode, toPort) {
// 防止两个节点之间出现多条连线
return fromNode.findLinksOutOf().all(function (link) {
console.log(link.toNode !== toNode)
return link.toNode !== toNode;
})
},
},
]
},
showPorts (node, show) {
let diagram = node.diagram
if (!diagram || diagram.isReadOnly || !diagram.allowLink) return
node.ports.each(function (port) {
port.stroke = (show ? 'white' : null)
})
},
model: function () {
return this.diagram.model
},
updateModel: function (val) {
// No GoJS transaction permitted when replacing Diagram.model.
if (val instanceof go.Model) {
this.diagram.model = val
} else {
let m = new go.GraphLinksModel()
if (val) {
for (let p in val) {
m[p] = val[p]
}
}
this.diagram.model = m
}
},
updateDiagramFromData: function () {
this.diagram.startTransaction()
// This is very general but very inefficient.
// It would be better to modify the diagramData data by calling
// Model.setDataProperty or Model.addNodeData, et al.
this.diagram.updateAllRelationshipsFromData()
this.diagram.updateAllTargetBindings()
this.diagram.commitTransaction('updated')
}
}
}
</script>
<style>
</style>
#### 父组件
<template>
<div >
<div class="div" style="display:flex;">
<div class="left" style="width: 60%;text-align: left;">
<diagram ref='diag' :model-data='diagramData' @model-changed='modelChanged' @changed-selection='changedSelection' @text-edited="textEdited" @modified="modified" style='width:100%; height:500px'></diagram>
</div>
<!-- <button @click='addNode'>Add Child to Gamma</button>
<button @click='modifyStuff'>Modify view model data without undo</button> -->
<div class="right" style="float:right">
<br/>Current Node:
<input v-model.lazy='currentNodeText' :disabled='currentNode === null'/>
</div>
</div>
<br/>The saved GoJS Model:
<!--<textarea style='width:100%;height:250px'>{{ savedModelText }}</textarea>-->
<textarea style='width:100%;height:200px' v-model="savedModelText"></textarea>
</div>
</template>
<script>
import diagram from '../components/GoDiagramWorkflow'
export default {
name: '',
components: {
diagram
},
data () {
return {
// diagramData: {},
diagramData2: {
'class': 'go.GraphLinksModel',
'linkFromPortIdProperty': 'fromPort',
'linkToPortIdProperty': 'toPort',
'nodeDataArray': [],
'linkDataArray': []
},
diagramData: {
'class': 'go.GraphLinksModel',
'linkFromPortIdProperty': 'fromPort',
'linkToPortIdProperty': 'toPort',
'nodeDataArray': [
{
'category': 'Start',
'text': '开始',
'key': 0,
'loc': '-202.90624999999994 -369.4999999999998'
},
{
'category': 'Pending',
'title': 'tsfsfsfsfsfsf',
'text': '源码构建',
'key': 2,
'loc': '-109.79687500000006 -248.24999999999994'
}
],
'linkDataArray': [{
'from': 0,
'to': 2,
'fromPort': 'B',
'toPort': 'T',
// 'text': 'up or timer',
'curviness': -20,
'points': [-202.90625, -353.06227569580085, -202.90625, -343.06227569580085, -202.90625, -308.875, -109.796875, -308.875, -109.796875, -274.6877243041992, -109.796875, -264.6877243041992]
}]
},
currentNode: null,
savedModelText: '',
counter: 1, // used by addNode
counter2: 4 // used by modifyStuff
}
},
mounted () {
},
computed: {
currentNodeText: {
get: function () {
let node = this.currentNode
console.log(window.go.Node)
if (node instanceof window.go.Node) {
console.log(node.data,)
return node.data.text
} else {
return ''
}
},
set: function (val) {
let node = this.currentNode
if (node instanceof window.go.Node) {
let model = this.model()
model.startTransaction()
model.setDataProperty(node.data, 'text', val)
model.commitTransaction('edited text')
}
}
}
},
methods: {
// get access to the GoJS Model of the GoJS Diagram
model: function () {
return this.$refs.diag.model()
},
// tell the GoJS Diagram to update based on the arbitrarily modified model data
updateDiagramFromData: function () {
this.$refs.diag.updateDiagramFromData()
},
// this event listener is declared on the
modelChanged: function (e) {
if (e.isTransactionFinished) { // show the model data in the page's TextArea
this.savedModelText = e.model.toJson()
}
},
changedSelection: function (e) {
let node = e.diagram.selection.first()
if (node instanceof window.go.Node) {
this.currentNode = node
this.currentNodeText = node.data.text
} else {
this.currentNode = null
this.currentNodeText = ''
}
},
textEdited: function (e) {
let data = this.diagramData
let nodeDataArray = data.nodeDataArray
let len = nodeDataArray.length
for (let i = 0; i < len; i++) {
nodeDataArray[i]['text'] = nodeDataArray[i]['text'].replace(/:/g, ':')
console.log(nodeDataArray[i]['text'])
}
this.updateDiagramFromData()
},
modified: function (e) {
},
// Here we modify the GoJS Diagram's Model using its methods,
// which can be much more efficient than modifying some memory and asking
// the GoJS Diagram to find differences and update accordingly.
// Undo and Redo will work as expected.
addNode: function () {
let model = this.model()
model.startTransaction()
model.setDataProperty(model.findNodeDataForKey(4), 'color', 'purple')
let data = { text: 'NEW ' + this.counter++, color: 'yellow' }
model.addNodeData(data)
model.addLinkData({ from: 3, to: model.getKeyForNodeData(data) })
model.commitTransaction('added Node and Link')
// also manipulate the Diagram by changing its Diagram.selection collection
let diagram = this.$refs.diag.diagram
diagram.select(diagram.findNodeForData(data))
},
// Here we modify VUE's view model directly, and
// then ask the GoJS Diagram to update everything from the data.
// This is less efficient than calling the appropriate GoJS Model methods.
// NOTE: Undo will not be able to restore all of the state properly!!
modifyStuff: function () {
let data = this.diagramData
data.nodeDataArray[0].color = 'red'
// Note here that because we do not have the GoJS Model,
// we cannot find out what values would be unique keys, for reference by the link data.
data.nodeDataArray.push({ key: ++this.counter2, text: this.counter2.toString(), color: 'orange' })
data.linkDataArray.push({ from: 2, to: this.counter2 })
this.updateDiagramFromData()
}
},
mounted(){
}
}
</script>
<style>
</style>