vue+gojs 流程圖
要實作的需求
流程圖,支援字型圖示,顔色,可連接配接線,點選時右側展示相關的詳細資訊

調研了多種可拖拽流程圖的技術,如:bpmn.js,gojs等,由于bpmn-js功能備援,GOJS相對于更加輕量級,最終選用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中引入
箭頭
許多連結确實希望通過使用箭頭來訓示方向性。 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);
hover效果顯示可連接配接點
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。
具體可檢視官網位址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 <diagram>
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>