547 lines
23 KiB
Vue
547 lines
23 KiB
Vue
<template>
|
||
<div v-if="easyFlowVisible" style="height: calc(100vh);">
|
||
<el-row>
|
||
<!--顶部工具菜单-->
|
||
<el-col :span="24">
|
||
<div class="ef-tooltar">
|
||
<el-link type="primary" :underline="false">{{data.name}}</el-link>
|
||
<el-divider direction="vertical"></el-divider>
|
||
<el-button type="text" icon="el-icon-delete" size="large" @click="deleteElement" :disabled="!this.activeElement.type"></el-button>
|
||
<el-divider direction="vertical"></el-divider>
|
||
<el-button type="text" icon="el-icon-download" size="large" @click="downloadData"></el-button>
|
||
<el-divider direction="vertical"></el-divider>
|
||
<el-button type="text" icon="el-icon-plus" size="large" @click="zoomAdd"></el-button>
|
||
<el-divider direction="vertical"></el-divider>
|
||
<el-button type="text" icon="el-icon-minus" size="large" @click="zoomSub"></el-button>
|
||
<div style="float: right;margin-right: 5px">
|
||
<el-button type="info" plain round icon="el-icon-document" @click="dataInfo" size="mini">流程信息</el-button>
|
||
<el-button type="primary" plain round @click="dataReloadA" icon="el-icon-refresh" size="mini">切换流程A</el-button>
|
||
<el-button type="primary" plain round @click="dataReloadB" icon="el-icon-refresh" size="mini">切换流程B</el-button>
|
||
<el-button type="primary" plain round @click="dataReloadC" icon="el-icon-refresh" size="mini">切换流程C</el-button>
|
||
<el-button type="primary" plain round @click="dataReloadD" icon="el-icon-refresh" size="mini">自定义样式</el-button>
|
||
<el-button type="primary" plain round @click="dataReloadE" icon="el-icon-refresh" size="mini">力导图</el-button>
|
||
<el-button type="info" plain round icon="el-icon-document" @click="openHelp" size="mini">帮助</el-button>
|
||
</div>
|
||
</div>
|
||
</el-col>
|
||
</el-row>
|
||
<div style="display: flex;height: calc(100% - 47px);">
|
||
<div style="width: 230px;border-right: 1px solid #dce3e8;">
|
||
<node-menu @addNode="addNode" ref="nodeMenu"></node-menu>
|
||
</div>
|
||
<div id="efContainer" ref="efContainer" class="container" v-flowDrag>
|
||
<template v-for="node in data.nodeList">
|
||
<flow-node
|
||
:id="node.id"
|
||
:key="node.id"
|
||
:node="node"
|
||
:activeElement="activeElement"
|
||
@changeNodeSite="changeNodeSite"
|
||
@nodeRightMenu="nodeRightMenu"
|
||
@clickNode="clickNode"
|
||
>
|
||
</flow-node>
|
||
</template>
|
||
<!-- 给画布一个默认的宽度和高度 -->
|
||
<div style="position:absolute;top: 2000px;left: 2000px;"> </div>
|
||
</div>
|
||
<!-- 右侧表单 -->
|
||
<div style="width: 300px;border-left: 1px solid #dce3e8;background-color: #FBFBFB">
|
||
<flow-node-form ref="nodeForm" @setLineLabel="setLineLabel" @repaintEverything="repaintEverything"></flow-node-form>
|
||
</div>
|
||
</div>
|
||
<!-- 流程数据详情 -->
|
||
<flow-info v-if="flowInfoVisible" ref="flowInfo" :data="data"></flow-info>
|
||
<flow-help v-if="flowHelpVisible" ref="flowHelp"></flow-help>
|
||
</div>
|
||
|
||
</template>
|
||
|
||
<script>
|
||
import draggable from 'vuedraggable'
|
||
// import { jsPlumb } from 'jsplumb'
|
||
// 使用修改后的jsplumb
|
||
import './jsplumb'
|
||
import { easyFlowMixin } from '@/components/ef/mixins'
|
||
import flowNode from '@/components/ef/node'
|
||
import nodeMenu from '@/components/ef/node_menu'
|
||
import FlowInfo from '@/components/ef/info'
|
||
import FlowHelp from '@/components/ef/help'
|
||
import FlowNodeForm from './node_form.vue'
|
||
import lodash from 'lodash'
|
||
import { getDataA } from './data_A'
|
||
import { getDataB } from './data_B'
|
||
import { getDataC } from './data_C'
|
||
import { getDataD } from './data_D'
|
||
import { getDataE } from './data_E'
|
||
import { ForceDirected } from './force-directed'
|
||
|
||
export default {
|
||
data() {
|
||
return {
|
||
// jsPlumb 实例
|
||
jsPlumb: null,
|
||
// 控制画布销毁
|
||
easyFlowVisible: true,
|
||
// 控制流程数据显示与隐藏
|
||
flowInfoVisible: false,
|
||
// 是否加载完毕标志位
|
||
loadEasyFlowFinish: false,
|
||
flowHelpVisible: false,
|
||
// 数据
|
||
data: {},
|
||
// 激活的元素、可能是节点、可能是连线
|
||
activeElement: {
|
||
// 可选值 node 、line
|
||
type: undefined,
|
||
// 节点ID
|
||
nodeId: undefined,
|
||
// 连线ID
|
||
sourceId: undefined,
|
||
targetId: undefined
|
||
},
|
||
zoom: 0.5
|
||
}
|
||
},
|
||
// 一些基础配置移动该文件中
|
||
mixins: [easyFlowMixin],
|
||
components: {
|
||
draggable, flowNode, nodeMenu, FlowInfo, FlowNodeForm, FlowHelp
|
||
},
|
||
directives: {
|
||
'flowDrag': {
|
||
bind(el, binding, vnode, oldNode) {
|
||
if (!binding) {
|
||
return
|
||
}
|
||
el.onmousedown = (e) => {
|
||
if (e.button == 2) {
|
||
// 右键不管
|
||
return
|
||
}
|
||
// 鼠标按下,计算当前原始距离可视区的高度
|
||
let disX = e.clientX
|
||
let disY = e.clientY
|
||
el.style.cursor = 'move'
|
||
|
||
document.onmousemove = function (e) {
|
||
// 移动时禁止默认事件
|
||
e.preventDefault()
|
||
const left = e.clientX - disX
|
||
disX = e.clientX
|
||
el.scrollLeft += -left
|
||
|
||
const top = e.clientY - disY
|
||
disY = e.clientY
|
||
el.scrollTop += -top
|
||
}
|
||
|
||
document.onmouseup = function (e) {
|
||
el.style.cursor = 'auto'
|
||
document.onmousemove = null
|
||
document.onmouseup = null
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
mounted() {
|
||
this.jsPlumb = jsPlumb.getInstance()
|
||
this.$nextTick(() => {
|
||
// 默认加载流程A的数据、在这里可以根据具体的业务返回符合流程数据格式的数据即可
|
||
this.dataReload(getDataB())
|
||
})
|
||
},
|
||
methods: {
|
||
// 返回唯一标识
|
||
getUUID() {
|
||
return Math.random().toString(36).substr(3, 10)
|
||
},
|
||
jsPlumbInit() {
|
||
this.jsPlumb.ready(() => {
|
||
// 导入默认配置
|
||
this.jsPlumb.importDefaults(this.jsplumbSetting)
|
||
// 会使整个jsPlumb立即重绘。
|
||
this.jsPlumb.setSuspendDrawing(false, true);
|
||
// 初始化节点
|
||
this.loadEasyFlow()
|
||
// 单点击了连接线, https://www.cnblogs.com/ysx215/p/7615677.html
|
||
this.jsPlumb.bind('click', (conn, originalEvent) => {
|
||
this.activeElement.type = 'line'
|
||
this.activeElement.sourceId = conn.sourceId
|
||
this.activeElement.targetId = conn.targetId
|
||
this.$refs.nodeForm.lineInit({
|
||
from: conn.sourceId,
|
||
to: conn.targetId,
|
||
label: conn.getLabel()
|
||
})
|
||
})
|
||
// 连线
|
||
this.jsPlumb.bind("connection", (evt) => {
|
||
let from = evt.source.id
|
||
let to = evt.target.id
|
||
if (this.loadEasyFlowFinish) {
|
||
this.data.lineList.push({from: from, to: to})
|
||
}
|
||
})
|
||
|
||
// 删除连线回调
|
||
this.jsPlumb.bind("connectionDetached", (evt) => {
|
||
this.deleteLine(evt.sourceId, evt.targetId)
|
||
})
|
||
|
||
// 改变线的连接节点
|
||
this.jsPlumb.bind("connectionMoved", (evt) => {
|
||
this.changeLine(evt.originalSourceId, evt.originalTargetId)
|
||
})
|
||
|
||
// 连线右击
|
||
this.jsPlumb.bind("contextmenu", (evt) => {
|
||
console.log('contextmenu', evt)
|
||
})
|
||
|
||
// 连线
|
||
this.jsPlumb.bind("beforeDrop", (evt) => {
|
||
let from = evt.sourceId
|
||
let to = evt.targetId
|
||
if (from === to) {
|
||
this.$message.error('节点不支持连接自己')
|
||
return false
|
||
}
|
||
if (this.hasLine(from, to)) {
|
||
this.$message.error('该关系已存在,不允许重复创建')
|
||
return false
|
||
}
|
||
if (this.hashOppositeLine(from, to)) {
|
||
this.$message.error('不支持两个节点之间连线回环');
|
||
return false
|
||
}
|
||
this.$message.success('连接成功')
|
||
return true
|
||
})
|
||
|
||
// beforeDetach
|
||
this.jsPlumb.bind("beforeDetach", (evt) => {
|
||
console.log('beforeDetach', evt)
|
||
})
|
||
this.jsPlumb.setContainer(this.$refs.efContainer)
|
||
})
|
||
},
|
||
// 加载流程图
|
||
loadEasyFlow() {
|
||
// 初始化节点
|
||
for (var i = 0; i < this.data.nodeList.length; i++) {
|
||
let node = this.data.nodeList[i]
|
||
// 设置源点,可以拖出线连接其他节点
|
||
this.jsPlumb.makeSource(node.id, lodash.merge(this.jsplumbSourceOptions, {}))
|
||
// // 设置目标点,其他源点拖出的线可以连接该节点
|
||
this.jsPlumb.makeTarget(node.id, this.jsplumbTargetOptions)
|
||
if (!node.viewOnly) {
|
||
this.jsPlumb.draggable(node.id, {
|
||
containment: 'parent',
|
||
stop: function (el) {
|
||
// 拖拽节点结束后的对调
|
||
console.log('拖拽结束: ', el)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
// 初始化连线
|
||
for (var i = 0; i < this.data.lineList.length; i++) {
|
||
let line = this.data.lineList[i]
|
||
var connParam = {
|
||
source: line.from,
|
||
target: line.to,
|
||
label: line.label ? line.label : '',
|
||
connector: line.connector ? line.connector : '',
|
||
anchors: line.anchors ? line.anchors : undefined,
|
||
paintStyle: line.paintStyle ? line.paintStyle : undefined,
|
||
}
|
||
this.jsPlumb.connect(connParam, this.jsplumbConnectOptions)
|
||
}
|
||
this.$nextTick(function () {
|
||
this.loadEasyFlowFinish = true
|
||
})
|
||
},
|
||
// 设置连线条件
|
||
setLineLabel(from, to, label) {
|
||
var conn = this.jsPlumb.getConnections({
|
||
source: from,
|
||
target: to
|
||
})[0]
|
||
if (!label || label === '') {
|
||
conn.removeClass('flowLabel')
|
||
conn.addClass('emptyFlowLabel')
|
||
} else {
|
||
conn.addClass('flowLabel')
|
||
}
|
||
conn.setLabel({
|
||
label: label,
|
||
})
|
||
this.data.lineList.forEach(function (line) {
|
||
if (line.from == from && line.to == to) {
|
||
line.label = label
|
||
}
|
||
})
|
||
|
||
},
|
||
// 删除激活的元素
|
||
deleteElement() {
|
||
if (this.activeElement.type === 'node') {
|
||
this.deleteNode(this.activeElement.nodeId)
|
||
} else if (this.activeElement.type === 'line') {
|
||
this.$confirm('确定删除所点击的线吗?', '提示', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
}).then(() => {
|
||
var conn = this.jsPlumb.getConnections({
|
||
source: this.activeElement.sourceId,
|
||
target: this.activeElement.targetId
|
||
})[0]
|
||
this.jsPlumb.deleteConnection(conn)
|
||
}).catch(() => {
|
||
})
|
||
}
|
||
},
|
||
// 删除线
|
||
deleteLine(from, to) {
|
||
this.data.lineList = this.data.lineList.filter(function (line) {
|
||
if (line.from == from && line.to == to) {
|
||
return false
|
||
}
|
||
return true
|
||
})
|
||
},
|
||
// 改变连线
|
||
changeLine(oldFrom, oldTo) {
|
||
this.deleteLine(oldFrom, oldTo)
|
||
},
|
||
// 改变节点的位置
|
||
changeNodeSite(data) {
|
||
for (var i = 0; i < this.data.nodeList.length; i++) {
|
||
let node = this.data.nodeList[i]
|
||
if (node.id === data.nodeId) {
|
||
node.left = data.left
|
||
node.top = data.top
|
||
}
|
||
}
|
||
},
|
||
/**
|
||
* 拖拽结束后添加新的节点
|
||
* @param evt
|
||
* @param nodeMenu 被添加的节点对象
|
||
* @param mousePosition 鼠标拖拽结束的坐标
|
||
*/
|
||
addNode(evt, nodeMenu, mousePosition) {
|
||
var screenX = evt.originalEvent.clientX, screenY = evt.originalEvent.clientY
|
||
let efContainer = this.$refs.efContainer
|
||
var containerRect = efContainer.getBoundingClientRect()
|
||
var left = screenX, top = screenY
|
||
// 计算是否拖入到容器中
|
||
if (left < containerRect.x || left > containerRect.width + containerRect.x || top < containerRect.y || containerRect.y > containerRect.y + containerRect.height) {
|
||
this.$message.error("请把节点拖入到画布中")
|
||
return
|
||
}
|
||
left = left - containerRect.x + efContainer.scrollLeft
|
||
top = top - containerRect.y + efContainer.scrollTop
|
||
// 居中
|
||
left -= 85
|
||
top -= 16
|
||
var nodeId = this.getUUID()
|
||
// 动态生成名字
|
||
var origName = nodeMenu.name
|
||
var nodeName = origName
|
||
var index = 1
|
||
while (index < 10000) {
|
||
var repeat = false
|
||
for (var i = 0; i < this.data.nodeList.length; i++) {
|
||
let node = this.data.nodeList[i]
|
||
if (node.name === nodeName) {
|
||
nodeName = origName + index
|
||
repeat = true
|
||
}
|
||
}
|
||
if (repeat) {
|
||
index++
|
||
continue
|
||
}
|
||
break
|
||
}
|
||
var node = {
|
||
id: nodeId,
|
||
name: nodeName,
|
||
type: nodeMenu.type,
|
||
left: left + 'px',
|
||
top: top + 'px',
|
||
ico: nodeMenu.ico,
|
||
state: 'success'
|
||
}
|
||
/**
|
||
* 这里可以进行业务判断、是否能够添加该节点
|
||
*/
|
||
this.data.nodeList.push(node)
|
||
this.$nextTick(function () {
|
||
this.jsPlumb.makeSource(nodeId, this.jsplumbSourceOptions)
|
||
this.jsPlumb.makeTarget(nodeId, this.jsplumbTargetOptions)
|
||
this.jsPlumb.draggable(nodeId, {
|
||
containment: 'parent',
|
||
stop: function (el) {
|
||
// 拖拽节点结束后的对调
|
||
console.log('拖拽结束: ', el)
|
||
}
|
||
})
|
||
})
|
||
},
|
||
/**
|
||
* 删除节点
|
||
* @param nodeId 被删除节点的ID
|
||
*/
|
||
deleteNode(nodeId) {
|
||
this.$confirm('确定要删除节点' + nodeId + '?', '提示', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning',
|
||
closeOnClickModal: false
|
||
}).then(() => {
|
||
/**
|
||
* 这里需要进行业务判断,是否可以删除
|
||
*/
|
||
this.data.nodeList = this.data.nodeList.filter(function (node) {
|
||
if (node.id === nodeId) {
|
||
// 伪删除,将节点隐藏,否则会导致位置错位
|
||
// node.show = false
|
||
return false
|
||
}
|
||
return true
|
||
})
|
||
this.$nextTick(function () {
|
||
this.jsPlumb.removeAllEndpoints(nodeId);
|
||
})
|
||
}).catch(() => {
|
||
})
|
||
return true
|
||
},
|
||
clickNode(nodeId) {
|
||
this.activeElement.type = 'node'
|
||
this.activeElement.nodeId = nodeId
|
||
this.$refs.nodeForm.nodeInit(this.data, nodeId)
|
||
},
|
||
// 是否具有该线
|
||
hasLine(from, to) {
|
||
for (var i = 0; i < this.data.lineList.length; i++) {
|
||
var line = this.data.lineList[i]
|
||
if (line.from === from && line.to === to) {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
},
|
||
// 是否含有相反的线
|
||
hashOppositeLine(from, to) {
|
||
return this.hasLine(to, from)
|
||
},
|
||
nodeRightMenu(nodeId, evt) {
|
||
this.menu.show = true
|
||
this.menu.curNodeId = nodeId
|
||
this.menu.left = evt.x + 'px'
|
||
this.menu.top = evt.y + 'px'
|
||
},
|
||
repaintEverything() {
|
||
this.jsPlumb.repaint()
|
||
},
|
||
// 流程数据信息
|
||
dataInfo() {
|
||
this.flowInfoVisible = true
|
||
this.$nextTick(function () {
|
||
this.$refs.flowInfo.init()
|
||
})
|
||
},
|
||
// 加载流程图
|
||
dataReload(data) {
|
||
this.easyFlowVisible = false
|
||
this.data.nodeList = []
|
||
this.data.lineList = []
|
||
this.$nextTick(() => {
|
||
data = lodash.cloneDeep(data)
|
||
this.easyFlowVisible = true
|
||
this.data = data
|
||
this.$nextTick(() => {
|
||
this.jsPlumb = jsPlumb.getInstance()
|
||
this.$nextTick(() => {
|
||
this.jsPlumbInit()
|
||
})
|
||
})
|
||
})
|
||
},
|
||
// 模拟载入数据dataA
|
||
dataReloadA() {
|
||
this.dataReload(getDataA())
|
||
},
|
||
// 模拟载入数据dataB
|
||
dataReloadB() {
|
||
this.dataReload(getDataB())
|
||
},
|
||
// 模拟载入数据dataC
|
||
dataReloadC() {
|
||
this.dataReload(getDataC())
|
||
},
|
||
// 模拟载入数据dataD
|
||
dataReloadD() {
|
||
this.dataReload(getDataD())
|
||
},
|
||
// 模拟加载数据dataE,自适应创建坐标
|
||
dataReloadE() {
|
||
let dataE = getDataE()
|
||
let tempData = lodash.cloneDeep(dataE)
|
||
let data = ForceDirected(tempData)
|
||
this.dataReload(data)
|
||
this.$message({
|
||
message: '力导图每次产生的布局是不一样的',
|
||
type: 'warning'
|
||
});
|
||
},
|
||
zoomAdd() {
|
||
if (this.zoom >= 1) {
|
||
return
|
||
}
|
||
this.zoom = this.zoom + 0.1
|
||
this.$refs.efContainer.style.transform = `scale(${this.zoom})`
|
||
this.jsPlumb.setZoom(this.zoom)
|
||
},
|
||
zoomSub() {
|
||
if (this.zoom <= 0) {
|
||
return
|
||
}
|
||
this.zoom = this.zoom - 0.1
|
||
this.$refs.efContainer.style.transform = `scale(${this.zoom})`
|
||
this.jsPlumb.setZoom(this.zoom)
|
||
},
|
||
// 下载数据
|
||
downloadData() {
|
||
this.$confirm('确定要下载该流程数据吗?', '提示', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning',
|
||
closeOnClickModal: false
|
||
}).then(() => {
|
||
var datastr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(this.data, null, '\t'));
|
||
var downloadAnchorNode = document.createElement('a')
|
||
downloadAnchorNode.setAttribute("href", datastr);
|
||
downloadAnchorNode.setAttribute("download", 'data.json')
|
||
downloadAnchorNode.click();
|
||
downloadAnchorNode.remove();
|
||
this.$message.success("正在下载中,请稍后...")
|
||
}).catch(() => {
|
||
})
|
||
},
|
||
openHelp() {
|
||
this.flowHelpVisible = true
|
||
this.$nextTick(function () {
|
||
this.$refs.flowHelp.init()
|
||
})
|
||
}
|
||
}
|
||
}
|
||
</script>
|