<template>
<div v-loading="loading" v-if="easyFlowVisible" class="box">
<div id="flowContainer" class="container">
<template v-for="node in data.nodeList">
<flow-node
v-show="node.show"
:id="node.id"
:node="node"
:key="node.id"
:nodeData="nodeData"
@deleteNode="deleteNode"
@changeNodeSite="changeNodeSite"
@nodeRightMenu="nodeRightMenu"
@editNode="editNode"
></flow-node>
</template>
</div>
<flow-info v-if="flowInfoVisible" ref="flowInfo" :data="data"></flow-info>
<flow-node-form v-if="nodeFormVisible" ref="nodeForm"></flow-node-form>
<div class="mask"></div>
</div>
</template>
<script>
import draggable from 'vuedraggable'
import { jsPlumb } from 'jsplumb'
import flowNode from '@/components/flow/node'
import flowTool from '@/components/flow/tool'
import FlowInfo from '@/components/flow/info'
import FlowNodeForm from './node_form'
import lodash from 'lodash'
import API from '@/api/build-management/index'
import { SvgTemperature } from '@hui/svg-icon'
export default {
name: 'easyFlow',
data () {
return {
loading: false,
jsPlumb: null, // jsPlumb 实例
easyFlowVisible: true,
flowInfoVisible: false,
nodeFormVisible: false,
index: 1,
// 默认设置参数
jsplumbSetting: {
// 动态锚点、位置自适应
Anchors: [
'Top',
'TopCenter',
'TopRight',
'TopLeft',
'Right',
'RightMiddle',
'Bottom',
'BottomCenter',
'BottomRight',
'BottomLeft',
'Left',
'LeftMiddle'
],
Container: 'flowContainer',
// 连线的样式 StateMachine、Flowchart
Connector: 'Flowchart',
// 鼠标不能拖动删除线
ConnectionsDetachable: false,
// 删除线的时候节点不删除
DeleteEndpointsOnDetach: false,
// 连线的端点
// Endpoint: ["Dot", {radius: 5}],
Endpoint: ['Rectangle', { height: 10, width: 10 }],
// 线端点的样式
EndpointStyle: { fill: 'rgba(255,255,255,0)', outlineWidth: 1 },
LogEnabled: true, // 是否打开jsPlumb的内部日志记录
// 绘制线
PaintStyle: { stroke: 'black', strokeWidth: 3 },
// 绘制箭头
Overlays: [['Arrow', { width: 12, length: 12, location: 1 }]],
RenderMode: 'svg'
},
// jsplumb连接参数
jsplumbConnectOptions: {
isSource: true,
isTarget: true
// 动态锚点、提供了4个方向 Continuous、AutoDefault
// anchor: 'Continuous'
// anchor: 'AutoDefault'
},
jsplumbSourceOptions: {
filter:
'.flow-node-drag' /* "span"表示标签,".className"表示类,"#id"表示元素id */,
filterExclude: false,
anchor: 'Continuous',
allowLoopback: false
},
jsplumbTargetOptions: {
filter:
'.flow-node-drag' /* "span"表示标签,".className"表示类,"#id"表示元素id */,
filterExclude: false,
anchor: 'Continuous',
allowLoopback: false
},
// 是否加载完毕
loadEasyFlowFinish: false,
// 数据
data: {},
nodeData: {},
levelTreeData: []
// exampleGreyEndpointOptions: {
// endpoint: 'Rectangle',
// paintStyle: { width: 25, height: 21, fillStyle: '#666' },
// isSource: true,
// connectorStyle: { strokeStyle: '#666' },
// isTarget: true
// }
}
},
props: {
flowData: Object
},
components: {
draggable,
flowNode,
flowTool,
FlowInfo,
FlowNodeForm
},
watch: {
flowData: {
// 图例在展示的时候,改变buildLevel或idAndNameDtos都会更新图例
handler: async function (val, oldVal) {
this.loading = true
const levelTreeRes = await API.levelTree()
this.levelTreeData = levelTreeRes.data
this.dataReloadC(this.transformData(val))
this.nodeData = val
setTimeout(() => {
this.loading = false
}, 500)
},
deep: true,
immediate: true
}
},
created () {
this.loading = true
},
async mounted () {
const levelTreeRes = await API.levelTree()
this.levelTreeData = levelTreeRes.data
this.jsPlumb = jsPlumb.getInstance()
this.$nextTick(() => {
// const data = {
// name: '流程',
// nodeList: [
// {
// id: '1',
// name: '园区',
// left: '100px',
// top: '15px',
// ico: 'h-icon-user',
// show: true
// },
// {
// id: '2',
// name: '学校',
// left: '100px',
// top: '140px',
// ico: 'h-icon-user',
// show: true
// },
// {
// id: '3',
// name: '建筑',
// left: '100px',
// top: '265px',
// ico: 'h-icon-user',
// show: true
// },
// {
// id: '4',
// name: '楼栋',
// left: '100px',
// top: '390px',
// ico: 'h-icon-user',
// show: true
// },
// {
// id: '5',
// name: '楼层',
// left: '100px',
// top: '515px',
// ico: 'h-icon-user',
// show: true
// },
// {
// id: '6',
// name: '房间',
// left: '100px',
// top: '640px',
// ico: 'h-icon-user',
// show: true
// }
// ],
// lineList: [
// {
// from: '1',
// to: '2'
// },
// {
// from: '2',
// to: '3'
// },
// {
// from: '3',
// to: '4'
// },
// {
// from: '4',
// to: '5'
// },
// {
// from: '5',
// to: '6'
// },
// {
// from: '3',
// to: '6',
// anchor: ['Right', 'Left']
// },
// {
// from: '3',
// to: '5',
// anchor: ['Right', 'Left']
// }
// ]
// }
this.nodeData = this.flowData
this.dataReloadC(this.transformData(this.flowData))
this.loading = false
})
},
methods: {
transformData (data) {
// const data = this.flowData
// const buildLevelListTemp = this.levelTreeData
const buildLevelListTemp = this.levelTreeData
const sortFun = (a, b) => a.buildLevel - b.buildLevel
const buildLevelList = buildLevelListTemp.sort(sortFun)
// 生成节点
const res = {
name: '流程',
nodeList: [],
lineList: []
}
res.nodeList = buildLevelList.map((item, index) => {
return {
id: item.buildTypeId,
name: item.buildName,
left: '100px',
top: `${15 + 125 * (item.buildLevel - 1)}px`,
ico: 'h-icon-user',
show: true,
buildLevel: item.buildLevel
}
})
res.nodeList.forEach((item, index) => {
if (index + 1 < res.nodeList.length) {
res.lineList.push({
from: item.id,
to: res.nodeList[index + 1].id
})
}
})
if (
Array.isArray(data.hierarchyForm.idAndNameDtos) &&
data.hierarchyForm.idAndNameDtos.length > 0
) {
data.hierarchyForm.idAndNameDtos.forEach((item, index) => {
if (
this.levelTreeData[data.hierarchyForm.buildLevel - 1] !== undefined
) {
res.lineList.push({
from: this.levelTreeData[data.hierarchyForm.buildLevel - 1]
.buildTypeId,
to: item,
anchor: ['Right', 'Left'],
paintStyle: { stroke: '#E72528', strokeWidth: 2 }
})
}
})
}
return res
},
jsPlumbInit () {
const _this = this
this.jsPlumb.ready(function () {
// 导入默认配置
_this.jsPlumb.importDefaults(_this.jsplumbSetting)
// 会使整个jsPlumb立即重绘。
_this.jsPlumb.setSuspendDrawing(false, true)
// 初始化节点
_this.loadEasyFlow()
// 单点击了连接线,
_this.jsPlumb.bind('click', function (conn, originalEvent) {
console.log('click', conn)
_this
.$confirm('确定删除所点击的线吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(() => {
_this.jsPlumb.deleteConnection(conn)
})
.catch(() => {})
})
// 连线
_this.jsPlumb.bind('connection', function (evt) {
console.log('connection', evt)
const from = evt.source.id
const to = evt.target.id
if (_this.loadEasyFlowFinish) {
_this.lineList.push({
from: from,
to: to
})
}
})
// 删除连线
_this.jsPlumb.bind('connectionDetached', function (evt) {
console.log('connectionDetached', evt)
_this.deleteLine(evt.sourceId, evt.targetId)
})
// 改变线的连接节点
_this.jsPlumb.bind('connectionMoved', function (evt) {
console.log('connectionMoved', evt)
_this.changeLine(evt.originalSourceId, evt.originalTargetId)
})
// 单击endpoint
// jsPlumb.bind("endpointClick", function (evt) {
// console.log('endpointClick', evt)
// })
//
// // 双击endpoint
// jsPlumb.bind("endpointDblClick", function (evt) {
// console.log('endpointDblClick', evt)
// })
// contextmenu
_this.jsPlumb.bind('contextmenu', function (evt) {
console.log('contextmenu', evt)
})
// beforeDrop
_this.jsPlumb.bind('beforeDrop', function (evt) {
console.log('beforeDrop', evt)
_this.$message.error('beforeDrop')
_this.$message({
message: '恭喜你,这是一条成功消息',
type: 'success'
})
const from = evt.sourceId
const 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
}
return true
})
// beforeDetach
_this.jsPlumb.bind('beforeDetach', function (evt) {
console.log('beforeDetach', evt)
})
})
},
// 加载流程图
loadEasyFlow () {
// 初始化节点
for (var i = 0; i < this.data.nodeList.length; i++) {
const node = this.data.nodeList[i]
console.log(node)
// 设置源点,可以拖出线连接其他节点
this.jsPlumb.makeSource(node.id, this.jsplumbSourceOptions)
// // 设置目标点,其他源点拖出的线可以连接该节点
this.jsPlumb.makeTarget(node.id, this.jsplumbTargetOptions)
// this.jsPlumb.addEndpoint(node.id, this.exampleGreyEndpointOptions)
// 设置可拖拽
// jsPlumb.draggable(node.id, {
// containment: 'parent',
// grid: [10, 10]
// })
this.jsPlumb.draggable(node.id, {
containment: 'parent'
})
// jsPlumb.draggable(node.id)
}
// 初始化连线
for (var i = 0; i < this.data.lineList.length; i++) {
const line = this.data.lineList[i]
this.jsPlumb.connect(
{
source: line.from,
target: line.to
},
Object.assign({
...this.jsplumbConnectOptions,
anchor: line.anchor,
paintStyle:
line.paintStyle === undefined
? { stroke: 'lightgray', strokeWidth: 1 }
: line.paintStyle
// connector: ['Bezier']
})
)
}
this.$nextTick(function () {
this.loadEasyFlowFinish = true
})
},
getNodes () {
console.log(jsPlumb)
console.log(jsPlumb.Defaults)
},
getLines () {
console.log('线', jsPlumb.getConnections())
},
// 删除线
deleteLine (from, to) {
this.data.lineList = this.data.lineList.filter(function (line) {
return line.from !== from && line.to !== to
})
},
// 改变连线
changeLine (oldFrom, oldTo) {
this.deleteLine(oldFrom, oldTo)
},
// 改变节点的位置
changeNodeSite (data) {
for (var i = 0; i < this.data.nodeList.length; i++) {
const node = this.data.nodeList[i]
if (node.id === data.nodeId) {
node.left = data.left
node.top = data.top
}
}
},
// 添加新的节点
addNode (evt, nodeMenu, mousePosition) {
console.log('添加节点', evt, nodeMenu)
const width = this.$refs.flowTool.$el.clientWidth
const index = this.index++
const nodeId = 'node' + index
var left = mousePosition.left
var top = mousePosition.top
if (mousePosition.left < 0) {
left = evt.originalEvent.layerX - width
}
if (mousePosition.top < 0) {
top = evt.originalEvent.clientY - 50
}
var node = {
id: 'node' + index,
name: '节点' + index,
left: left + 'px',
top: top + 'px',
ico: nodeMenu.ico,
show: true
}
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'
})
})
},
// 是否具有该线
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'
},
deleteNode (nodeId) {
this.$confirm('确定要删除节点' + nodeId + '?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
closeOnClickModal: false
})
.then(() => {
this.data.nodeList = this.data.nodeList.filter(function (node) {
// return node.id !== nodeId
if (node.id === nodeId) {
node.show = false
}
return true
})
this.$nextTick(function () {
console.log('删除' + nodeId)
this.jsPlumb.removeAllEndpoints(nodeId)
})
})
.catch(() => {})
return true
},
editNode (nodeId) {
console.log('编辑节点', nodeId)
this.nodeFormVisible = true
this.$nextTick(function () {
this.$refs.nodeForm.init(this.data, nodeId)
})
},
// 流程数据信息
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()
})
})
})
},
// 数据重新载入
dataReloadC (data) {
this.dataReload(data)
}
}
}
</script>
<style lang="scss" scoped>
.box {
height: calc(100% - 35px);
position: relative;
.mask {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 1000;
}
.container {
// background-image: linear-gradient(
// 90deg,
// rgba(0, 0, 0, 0.15) 10%,
// rgba(0, 0, 0, 0) 10%
// ),
// linear-gradient(rgba(0, 0, 0, 0.15) 10%, rgba(0, 0, 0, 0) 10%);
background-size: 10px 10px;
height: 100%;
background-color: rgb(251, 251, 251);
/*background-color: #42b983;*/
position: relative;
}
.labelClass {
background-color: white;
padding: 5px;
opacity: 0.7;
border: 1px solid #346789;
/*border-radius: 10px;*/
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
}
</style>
flowNode
<template>
<div
ref="node"
:style="flowNodeContainer"
@mouseenter="showDelete"
@mouseleave="hideDelete"
@mouseup="changeNodeSite"
>
<!-- <div class="flow-node-header"> -->
<!--左上角的那个图标样式-->
<!-- <i :class="nodeClass"></i> -->
<!--鼠标移入到节点中时右上角的【编辑】、【删除】 按钮-->
<!-- <div style="position: absolute;top: 0px;right: 0px;line-height: 25px" v-show="mouseEnter">
<a href="#" style @click="editNode">
<img src="@/assets/edit.png" />
</a>
<a href="#" style @click="deleteNode">
<img src="@/assets/delete.png" />
</a>
</div>-->
<!-- </div> -->
<!--节点的正文部分-->
<!-- <div class="flow-node-body">{{node.name}}</div> -->
<div class="box">
<div :class="['flow-node',activeStyle]">
<div class="flow-node-left">
<img src="@/assets/img_层级编号.png" alt />
<span>{{node.buildLevel}}</span>
</div>
<div class="flow-node-right">
<span>{{node.name}}</span>
</div>
</div>
<div v-if="activeDot" class="dot"></div>
</div>
</div>
</template>
<script>
export default {
props: {
node: Object,
nodeData: Object
},
data () {
return {
// 控制节点操作显示
mouseEnter: false
}
},
computed: {
// 节点容器样式
flowNodeContainer: {
get () {
return {
position: 'absolute',
width: '274px',
top: this.node.top,
left: this.node.left,
boxShadow: this.mouseEnter ? '#66a6e0 0px 0px 12px 0px' : '',
backgroundColor: 'transparent'
}
}
},
nodeClass () {
var nodeclass = {}
nodeclass[this.node.ico] = true
nodeclass['flow-node-drag'] = true
return nodeclass
},
activeStyle () {
return this.node.buildLevel === this.nodeData.hierarchyForm.buildLevel ? 'active' : ''
},
activeDot () {
return (this.nodeData.hierarchyForm.idAndNameDtos.length > 0) && (
this.node.buildLevel === this.nodeData.hierarchyForm.buildLevel || this.nodeData.hierarchyForm.idAndNameDtos.find(
id => {
return id === this.node.id
}
)
)
}
},
watch: {
},
mounted () {
console.log(this.node)
console.log(this.nodeData)
},
methods: {
// 删除节点
deleteNode () {
this.$emit('deleteNode', this.node.id)
},
// 编辑节点
editNode () {
this.$emit('editNode', this.node.id)
},
// 鼠标进入
showDelete () {
this.mouseEnter = true
},
// 鼠标离开
hideDelete () {
this.mouseEnter = false
},
// 鼠标移动后抬起
changeNodeSite () {
// 避免抖动
if (this.node.left == this.$refs.node.style.left && this.node.top == this.$refs.node.style.top) {
return
}
this.$emit('changeNodeSite', {
nodeId: this.node.id,
left: this.$refs.node.style.left,
top: this.$refs.node.style.top
})
}
}
}
</script>
<style lang="scss" scoped>
.box {
width: 100%;
padding-right: 18px;
position: relative;
.dot {
position: absolute;
right: 0px;
top: 18px;
width: 14px;
height: 14px;
border-radius: 7px;
background: #e72528;
&:after {
content: '';
width: 4px;
height: 4px;
border-radius: 2px;
background: #fff;
position: absolute;
right: 5px;
top: 5px;
}
}
.flow-node {
width: 100%;
height: 50px;
background: #ffffff;
border: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 2px;
overflow: hidden;
position: relative;
.flow-node-left {
width: 48px;
height: 48px;
// background: rgba(0, 0, 0, 0.9);
border-radius: 2px NaNpx 2px;
float: left;
position: relative;
img {
position: relative;
// top: 2px;
}
span {
position: absolute;
left: 18px;
top: 11px;
width: 8px;
height: 26px;
font-family: PingFangSC-Medium;
font-size: 18px;
color: rgba(255, 255, 255, 0.9);
line-height: 26px;
font-weight: 500;
}
}
.flow-node-right {
height: 48px;
line-height: 50px;
font-family: PingFangSC-Regular;
font-size: 14px;
color: rgba(0, 0, 0, 0.7);
line-height: 20px;
font-weight: 400;
float: left;
span {
display: inline-block;
height: 48px;
line-height: 48px;
margin-left: 16px;
max-width: 188px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
}
.active {
background: rgba(231, 37, 40, 0.2);
border: 2px solid #e72528;
box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12),
0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 9px 28px 8px rgba(0, 0, 0, 0.05);
border-radius: 2px;
position: relative;
}
}
// .flow-node-drag {
// width: 25px;
// height: 25px;
// }
// .flow-node-header {
// background-color: #66a6e0;
// height: 25px;
// cursor: pointer;
// border-top-left-radius: 6px;
// border-top-right-radius: 6px;
// }
// .flow-node-header a {
// text-decoration: none;
// line-height: 25px;
// vertical-align: middle;
// }
// .flow-node-header a img {
// line-height: 25px;
// vertical-align: middle;
// margin-bottom: 5px;
// }
// .flow-node-body {
// /*background-color: beige;*/
// background-color: white;
// text-align: center;
// cursor: pointer;
// height: 25px;
// line-height: 25px;
// border-bottom-left-radius: 6px;
// border-bottom-right-radius: 6px;
// }
</style>