feat: add flowChart components

This commit is contained in:
xiaoxian521
2021-04-15 23:10:28 +08:00
parent f886a38694
commit c7f6ff6514
21 changed files with 1029 additions and 17 deletions

View File

View File

@@ -0,0 +1,115 @@
<template>
<el-tabs tab-position="left">
<el-tab-pane label="添加动作">
<div v-for="item in nodeList" :key="item.type">
<el-button
class="add-node-btn"
type="primary"
size="mini"
@click="$_addNode(item)"
>{{item.label}}</el-button>
</div>
</el-tab-pane>
<el-tab-pane label="添加组">
<el-button class="add-node-btn" type="primary" size="mini" @click="$_addTempalte">模板</el-button>
</el-tab-pane>
</el-tabs>
</template>
<script>
export default {
name: 'AddPanel',
props: {
nodeData: Object,
lf: Object || String
},
data() {
return {
nodeList: [
{
type: 'user',
label: '用户'
},
{
type: 'push',
label: '推送'
}
]
}
},
methods: {
$_addNode(item) {
const { lf, nodeData } = this.$props
const { id, x, y } = nodeData
const nextNode = lf.addNode({
type: item.type,
x: x + 150,
y: y + 150
})
const nextId = nextNode.id
lf.createEdge({ sourceNodeId: id, targetNodeId: nextId })
this.$emit('addNodeFinish')
},
$_addTempalte() {
const { lf, nodeData } = this.$props
const { id, x, y } = nodeData
const timeNode = lf.addNode({
type: 'download',
x,
y: y + 150
})
const userNode = lf.addNode({
type: 'user',
x: x + 150,
y: y + 150
})
const pushNode = lf.addNode({
type: 'push',
x: x + 150,
y: y + 300,
properties: {}
})
const endNode = lf.addNode({
type: 'end',
x: x + 300,
y: y + 150
})
const endNode2 = lf.addNode({
type: 'end',
x: x + 300,
y: y + 300
})
lf.createEdge({ sourceNodeId: id, targetNodeId: timeNode.id })
lf.createEdge({ sourceNodeId: timeNode.id, targetNodeId: userNode.id })
lf.createEdge({
sourceNodeId: userNode.id,
targetNodeId: endNode.id,
endPoint: { x: x + 280, y: y + 150 },
text: {
value: 'Y',
x: x + 230,
y: y + 140
}
})
lf.createEdge({
sourceNodeId: userNode.id,
targetNodeId: pushNode.id,
text: {
value: 'N',
x: x + 160,
y: y + 230
}
})
lf.createEdge({ sourceNodeId: pushNode.id, targetNodeId: endNode2.id, endPoint: { x: x + 280, y: y + 300 } })
this.$emit('addNodeFinish')
}
}
}
</script>
<style scoped>
.add-node-btn {
margin-bottom: 10px;
margin-right: 20px;
}
</style>

View File

View File

@@ -0,0 +1,74 @@
<template>
<div>
<el-button-group>
<el-button size="small" @click="$_zoomIn">放大</el-button>
<el-button size="small" @click="$_zoomOut">缩小</el-button>
<el-button size="small" @click="$_zoomReset">大小适应</el-button>
<el-button size="small" @click="$_translateRest">定位还原</el-button>
<el-button size="small" @click="$_reset">还原(大小&定位)</el-button>
<el-button size="small" @click="$_undo" :disabled="undoDisable">上一步(ctrl+z)</el-button>
<el-button size="small" @click="$_redo" :disabled="redoDisable">下一步(ctrl+y)</el-button>
<el-button size="small" @click="$_download">下载图片</el-button>
<el-button size="small" @click="$_catData">查看数据</el-button>
<el-button v-if="catTurboData" size="small" @click="$_catTurboData">查看turbo数据</el-button>
</el-button-group>
</div>
</template>
<script>
export default {
name: 'Control',
props: {
lf: Object || String,
catTurboData: Boolean
},
data() {
return {
undoDisable: true,
redoDisable: true,
}
},
mounted() {
this.$props.lf.on('history:change', ({ data: { undoAble, redoAble } }) => {
this.$data.undoDisable = !undoAble
this.$data.redoDisable = !redoAble
})
},
methods: {
$_zoomIn() {
this.$props.lf.zoom(true)
},
$_zoomOut() {
this.$props.lf.zoom(false)
},
$_zoomReset() {
this.$props.lf.resetZoom()
},
$_translateRest() {
this.$props.lf.resetTranslate()
},
$_reset() {
this.$props.lf.resetZoom()
this.$props.lf.resetTranslate()
},
$_undo() {
this.$props.lf.undo()
},
$_redo() {
this.$props.lf.redo()
},
$_download() {
this.$props.lf.getSnapshot()
},
$_catData() {
this.$emit('catData')
},
$_catTurboData() {
this.$emit('catTurboData')
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,22 @@
<template>
<div>
<vue-json-pretty :path="'res'" :deep="3" :showLength="true" :data="graphData"></vue-json-pretty>
</div>
</template>
<script>
import VueJsonPretty from 'vue-json-pretty'
import 'vue-json-pretty/lib/styles.css'
export default {
props: {
graphData: Object
},
components: {
VueJsonPretty,
},
};
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,124 @@
<template>
<div class="node-panel">
<div class="node-item" v-for="item in nodeList" :key="item.text" @mousedown="$_dragNode(item)">
<div class="node-item-icon" :class="item.class">
<div v-if="item.type === 'user' || item.type === 'time'" class="shape"></div>
</div>
<span class="node-label">{{item.text}}</span>
</div>
</div>
</template>
<script>
export default {
name: 'NodePanel',
props: {
lf: Object,
nodeList: Array,
},
data() {
return {
node: {
type: 'rect',
property: {
a: 'efrwe',
b: 'wewe'
}
},
properties: {
a: 'efrwe',
b: 'wewe'
}
}
},
methods: {
$_dragNode(item) {
this.$props.lf.dnd.startDrag({
type: item.type,
properties: this.$data.properties
})
}
}
}
</script>
<style>
.node-panel {
position: absolute;
top: 100px;
left: 50px;
width: 70px;
padding: 20px 10px;
background-color: white;
box-shadow: 0 0 10px 1px rgb(228, 224, 219);
border-radius: 6px;
text-align: center;
z-index: 101;
}
.node-item {
margin-bottom: 20px;
}
.node-item-icon {
height: 30px;
display: flex;
flex-wrap: wrap;
justify-content: center;
background-size: cover;
}
.node-label {
font-size: 12px;
margin-top: 5px;
user-select: none;
}
.node-start {
background: url("./background/start.png") no-repeat;
background-size: cover;
}
.node-rect {
border: 1px solid black;
}
.node-user {
background: url("./background/user.png") no-repeat;
background-size: cover;
}
.node-time {
background: url("./background/time.png") no-repeat;
background-size: cover;
}
.node-push {
background: url("./background/push.png") no-repeat;
background-size: cover;
}
.node-download {
background: url("./background/download.png") no-repeat;
background-size: cover;
}
.node-click {
background: url("./background/click.png") no-repeat;
background-size: cover;
}
.node-end {
background: url("./background/end.png") no-repeat;
background-size: cover;
}
.bpmn-start {
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAAH6ji2bAAAABGdBTUEAALGPC/xhBQAAAnBJREFUOBGdVL1rU1EcPfdGBddmaZLiEhdx1MHZQXApraCzQ7GKLgoRBxMfcRELuihWKcXFRcEWF8HBf0DdDCKYRZpnl7p0svLe9Zzbd29eQhTbC8nv+9zf130AT63jvooOGS8Vf9Nt5zxba7sXQwODfkWpkbjTQfCGUd9gIp3uuPP8bZ946g56dYQvnBg+b1HB8VIQmMFrazKcKSvFW2dQTxJnJdQ77urmXWOMBCmXM2Rke4S7UAW+/8ywwFoewmBps2tu7mbTdp8VMOkIRAkKfrVawalJTtIliclFbaOBqa0M2xImHeVIfd/nKAfVq/LGnPss5Kh00VEdSzfwnBXPUpmykNss4lUI9C1ga+8PNrBD5YeqRY2Zz8PhjooIbfJXjowvQJBqkmEkVnktWhwu2SM7SMx7Cj0N9IC0oQXRo8xwAGzQms+xrB/nNSUWVveI48ayrFGyC2+E2C+aWrZHXvOuz+CiV6iycWe1Rd1Q6+QUG07nb5SbPrL4426d+9E1axKjY3AoRrlEeSQo2Eu0T6BWAAr6COhTcWjRaYfKG5csnvytvUr/WY4rrPMB53Uo7jZRjXaG6/CFfNMaXEu75nG47X+oepU7PKJvvzGDY1YLSKHJrK7vFUwXKkaxwhCW3u+sDFMVrIju54RYYbFKpALZAo7sB6wcKyyrd+aBMryMT2gPyD6GsQoRFkGHr14TthZni9ck0z+Pnmee460mHXbRAypKNy3nuMdrWgVKj8YVV8E7PSzp1BZ9SJnJAsXdryw/h5ctboUVi4AFiCd+lQaYMw5z3LGTBKjLQOeUF35k89f58Vv/tGh+l+PE/wG0rgfIUbZK5AAAAABJRU5ErkJggg==)
center center no-repeat;
cursor: grab;
}
.bpmn-end {
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAAH6ji2bAAAABGdBTUEAALGPC/xhBQAAA1BJREFUOBFtVE1IVUEYPXOf+tq40Y3vPcmFIdSjIorWoRG0ERWUgnb5FwVhYQSl72oUoZAboxKNFtWiwKRN0M+jpfSzqJAQclHo001tKkjl3emc8V69igP3znzfnO/M9zcDcKT67azmjYWTwl9Vn7Vumeqzj1DVb6cleQY4oAVnIOPb+mKAGxQmKI5CWNJ2aLPatxWa3aB9K7/fB+/Z0jUF6TmMlFLQqrkECWQzOZxYGjTlOl8eeKaIY5yHnFn486xBustDjWT6dG7pmjHOJd+33t0iitTPkK6tEvjxq4h2MozQ6WFSX/LkDUGfFwfhEZj1Auz/U4pyAi5Sznd7uKzznXeVHlI/Aywmk6j7fsUsEuCGADrWARXXwjxWQsUbIupDHJI7kF5dRktg0eN81IbiZXiTESic50iwS+t1oJgL83jAiBupLDCQqwziaWSoAFSeIR3P5Xv5az00wyIn35QRYTwdSYbz8pH8fxUUAtxnFvYmEmgI0wYXUXcCCSpeEVpXlsRhBnCEATxWylL9+EKCAYhe1NGstUa6356kS9NVvt3DU2fd+Wtbm/+lSbylJqsqkSm9CRhvoJVlvKPvF1RKY/FcPn5j4UfIMLn8D4UYb54BNsilTDXKnF4CfTobA0FpoW/LSp306wkXM+XaOJhZaFkcNM82ASNAWMrhrUbRfmyeI1FvRBTpN06WKxa9BK0o2E4Pd3zfBBEwPsv9sQBnmLVbLEIZ/Xe9LYwJu/Er17W6HYVBc7vmuk0xUQ+pqxdom5Fnp55SiytXLPYoMXNM4u4SNSCFWnrVIzKG3EGyMXo6n/BQOe+bX3FClY4PwydVhthOZ9NnS+ntiLh0fxtlUJHAuGaFoVmttpVMeum0p3WEXbcll94l1wM/gZ0Ccczop77VvN2I7TlsZCsuXf1WHvWEhjO8DPtyOVg2/mvK9QqboEth+7pD6NUQC1HN/TwvydGBARi9MZSzLE4b8Ru3XhX2PBxf8E1er2A6516o0w4sIA+lwURhAON82Kwe2iDAC1Watq4XHaGQ7skLcFOtI5lDxuM2gZe6WFIotPAhbaeYlU4to5cuarF1QrcZ/lwrLaCJl66JBocYZnrNlvm2+MBCTmUymPrYZVbjdlr/BxlMjmNmNI3SAAAAAElFTkSuQmCC)
center center no-repeat;
cursor: grab;
}
.bpmn-user {
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAEFVwZaAAAABGdBTUEAALGPC/xhBQAAAqlJREFUOBF9VM9rE0EUfrMJNUKLihGbpLGtaCOIR8VjQMGDePCgCCIiCNqzCAp2MyYUCXhUtF5E0D+g1t48qAd7CCLqQUQKEWkStcEfVGlLdp/fm3aW2QQdyLzf33zz5m2IsAZ9XhDpyaaIZkTS4ASzK41TFao88GuJ3hsr2pAbipHxuSYyKRugagICGANkfFnNh3HeE2N0b3nN2cgnpcictw5veJIzxmDamSlxxQZicq/mflxhbaH8BLRbuRwNtZp0JAhoplVRUdzmCe/vO27wFuuA3S5qXruGdboy5/PRGFsbFGKo/haRtQHIrM83bVeTrOgNhZReWaYGnE4aUQgTJNvijJFF4jQ8BxJE5xfKatZWmZcTQ+BVgh7s8SgPlCkcec4mGTmieTP4xd7PcpIEg1TX6gdeLW8rTVMVLVvb7ctXoH0Cydl2QOPJBG21STE5OsnbweVYzAnD3A7PVILuY0yiiyDwSm2g441r6rMSgp6iK42yqroI2QoXeJVeA+YeZSa47gZdXaZWQKTrG93rukk/l2Al6Kzh5AZEl7dDQy+JjgFahQjRopSxPbrbvK7GRe9ePWBo1wcU7sYrFZtavXALwGw/7Dnc50urrHJuTPSoO2IMV3gUQGNg87IbSOIY9BpiT9HV7FCZ94nPXb3MSnwHn/FFFE1vG6DTby+r31KAkUktB3Qf6ikUPWxW1BkXSPQeMHHiW0+HAd2GelJsZz1OJegCxqzl+CLVHa/IibuHeJ1HAKzhuDR+ymNaRFM+4jU6UWKXorRmbyqkq/D76FffevwdCp+jN3UAN/C9JRVTDuOxC/oh+EdMnqIOrlYteKSfadVRGLJFJPSB/ti/6K8f0CNymg/iH2gO/f0DwE0yjAFO6l8JaR5j0VPwPwfaYHqOqrCI319WzwhwzNW/aQAAAABJRU5ErkJggg==)
center center no-repeat;
cursor: grab;
}
.bpmn-exclusiveGateway {
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABUAAAAVCAYAAAHeEJUAAAAABGdBTUEAALGPC/xhBQAAAvVJREFUOBGNVEFrE0EU/mY3bQoiFlOkaUJrQUQoWMGePLX24EH0IIoHKQiCV0G8iE1covgLiqA/QTzVm1JPogc9tIJYFaQtlhQxqYjSpunu+L7JvmUTU3AgmTfvffPNN++9WSA1DO182f6xwILzD5btfAoQmwL5KJEwiQyVbSVZ0IgRyV6PTpIJ81E5ZvqfHQR0HUOBHW4L5Et2kQ6Zf7iAOhTFAA8s0pEP7AXO1uAA52SbqGk6h/6J45LaLhO64ByfcUzM39V7ZiAdS2yCePPEIQYvTUHqM/n7dgQNfBKWPjpF4ISk8q3J4nB11qw6X8l+FsF3EhlkEMfrjIer3wJTLwS2aCNcj4DbGxXTw00JmAuO+Ni6bBxVUCvS5d9aa04+so4pHW5jLTywuXAL7jJ+D06sl82Sgl2JuVBQn498zkc2bGKxULHjCnSMadBKYDYYHAtsby1EQ5lNGrQd4Y3v4Zo0XdGEmDno46yCM9Tk+RiJmUYHS/aXHPNTcjxcbTFna000PFJHIVZ5lFRqRpJWk9/+QtlOUYJj9HG5pVFEU7zqIYDVsw2s+AJaD8wTd2umgSCCyUxgGsS1Y6TBwXQQTFuZaHcd8gAGioE90hlsY+wMcs30RduYtxanjMGal8H5dMW67dmT1JFtYUEe8LiQLRsPZ6IIc7A4J5tqco3T0pnv/4u0kyzrYUq7gASuEyI8VXKvB9Odytv6jS/PNaZBln0nioJG/AVQRZvApOdhjj3Jt8QC8Im09SafwdBdvIpztpxWxpeKCC+EsFdS8DCyuCn2munFpL7ctHKp+Xc5cMybeIyMAN33SPL3ZR9QV1XVwLyzHm6Iv0/yeUuUb7PPlZC4D4HZkeu6dpF4v9j9MreGtMbxMMRLIcjJic9yHi7WQ3yVKzZVWUr5UrViJvn1FfUlwe/KYVfYyWRLSGNu16hR01U9IacajXPei0wx/5BqgInvJN+MMNtNme7ReU9SBbgntovn0kKHpFg7UogZvaZiOue/q1SBo9ktHzQAAAAASUVORK5CYII=)
center center no-repeat;
cursor: grab;
}
</style>

View File

@@ -0,0 +1,166 @@
const TurboType = {
SEQUENCE_FLOW: 1,
START_EVENT: 2,
END_EVENT: 3,
USER_TASK: 4,
SERVICE_TASK: 5,
EXCLUSIVE_GATEWAY: 6,
}
function getTurboType(type) {
switch (type) {
case 'bpmn:sequenceFlow':
return TurboType.SEQUENCE_FLOW
case 'bpmn:startEvent':
return TurboType.START_EVENT
case 'bpmn:endEvent':
return TurboType.END_EVENT
case 'bpmn:userTask':
return TurboType.USER_TASK
case 'bpmn:serviceTask':
return TurboType.SERVICE_TASK
case 'bpmn:exclusiveGateway':
return TurboType.EXCLUSIVE_GATEWAY
default:
return type
}
}
function convertNodeToTurboElement(node) {
const { id, type, x, y, text = '', properties } = node
return {
incoming: [],
outgoing: [],
dockers: [],
type: getTurboType(node.type),
properties: {
...properties,
name: text && text.value || '',
x: x,
y: y,
text,
logicFlowType: type,
},
key: id,
}
}
function convertEdgeToTurboElement(edge) {
const {
id,
type,
sourceNodeId,
targetNodeId,
startPoint,
endPoint,
pointsList,
text = '',
properties } = edge
return {
incoming: [sourceNodeId],
outgoing: [targetNodeId],
type: getTurboType(type),
dockers: [],
properties: {
...properties,
name: text && text.value || '',
text,
startPoint,
endPoint,
pointsList,
logicFlowType: type,
},
key: id,
}
}
export function toTurboData(data) {
const nodeMap = new Map()
const turboData = {
flowElementList: [],
}
data.nodes.forEach((node) => {
const flowElement = convertNodeToTurboElement(node)
turboData.flowElementList.push(flowElement)
nodeMap.set(node.id, flowElement)
})
data.edges.forEach((edge) => {
const flowElement = convertEdgeToTurboElement(edge)
const sourceElement = nodeMap.get(edge.sourceNodeId)
sourceElement.outgoing.push(flowElement.key)
const targetElement = nodeMap.get(edge.targetNodeId)
targetElement.incoming.push(flowElement.key)
turboData.flowElementList.push(flowElement)
})
return turboData
}
function convertFlowElementToEdge(element) {
const { incoming, outgoing, properties, key } = element
const {
text,
startPoint,
endPoint,
pointsList,
logicFlowType
} = properties
const edge = {
id: key,
type: logicFlowType,
sourceNodeId: incoming[0],
targetNodeId: outgoing[0],
text,
startPoint,
endPoint,
pointsList,
properties: {}
}
const excludeProperties = ['startPoint', 'endPoint', 'pointsList', 'text', 'logicFlowType']
Object.keys(element.properties).forEach(property => {
if (excludeProperties.indexOf(property) === -1) {
edge.properties[property] = element.properties[property]
}
})
return edge
}
function convertFlowElementToNode(element) {
const { properties, key } = element
const { x, y, text, logicFlowType } = properties
const node = {
id: key,
type: logicFlowType,
x,
y,
text,
properties: {}
}
const excludeProperties = ['x', 'y', 'text', 'logicFlowType']
Object.keys(element.properties).forEach(property => {
if (excludeProperties.indexOf(property) === -1) {
node.properties[property] = element.properties[property]
}
})
return node
}
export function toLogicflowData(data) {
const lfData = {
nodes: [],
edges: [],
}
const list = data.flowElementList
list && list.length > 0 && list.forEach(element => {
if (element.type === TurboType.SEQUENCE_FLOW) {
const edge = convertFlowElementToEdge(element)
lfData.edges.push(edge)
} else {
const node = convertFlowElementToNode(element)
lfData.nodes.push(node)
}
})
return lfData
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

@@ -0,0 +1,55 @@
export const nodeList = [
{
text: '开始',
type: 'start',
class: 'node-start'
},
{
text: '矩形',
type: 'rect',
class: 'node-rect'
},
{
type: 'user',
text: '用户',
class: 'node-user'
},
{
type: 'push',
text: '推送',
class: 'node-push'
},
{
type: 'download',
text: '位置',
class: 'node-download'
},
{
type: 'end',
text: '结束',
class: 'node-end'
},
]
export const BpmnNode = [
{
type: 'bpmn:startEvent',
text: '开始',
class: 'bpmn-start'
},
{
type: 'bpmn:endEvent',
text: '结束',
class: 'bpmn-end'
},
{
type: 'bpmn:exclusiveGateway',
text: '网关',
class: 'bpmn-exclusiveGateway'
},
{
type: 'bpmn:userTask',
text: '用户',
class: 'bpmn-user'
},
]