mirror of
https://gitee.com/xiangheng/x_admin.git
synced 2025-12-24 08:12:55 +08:00
324 lines
9.3 KiB
Vue
324 lines
9.3 KiB
Vue
<template>
|
||
<div class="diagram">
|
||
<diagram-toolbar
|
||
v-if="lf"
|
||
class="diagram-toolbar"
|
||
:lf="lf"
|
||
:active-edges="activeEdges"
|
||
@saveGraph="saveGraph"
|
||
@importData="importData"
|
||
/>
|
||
<div class="diagram-main">
|
||
<diagram-sidebar class="diagram-sidebar" @dragInNode="dragInNode" />
|
||
<div ref="container" class="diagram-container">
|
||
<div class="diagram-wrapper">
|
||
<div ref="diagramRef" class="lf-diagram"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- Right-side property panel -->
|
||
<PropertyPanel ref="PropertyPanelRef" @setProperties="setProperties" />
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
// Importing necessary functions and components
|
||
import { ref, onMounted } from 'vue'
|
||
import LogicFlow from '@logicflow/core'
|
||
import { SelectionSelect, Menu, BpmnElement } from '@logicflow/extension'
|
||
|
||
import '@logicflow/core/dist/style/index.css'
|
||
import '@logicflow/extension/lib/style/index.css'
|
||
import DiagramToolbar from './DiagramToolbar.vue'
|
||
import DiagramSidebar from './DiagramSidebar.vue'
|
||
import PropertyPanel from './PropertyPanel/PropertyPanel.vue'
|
||
import { registerCustomElement } from './node'
|
||
|
||
defineOptions({
|
||
name: 'flowEdit'
|
||
})
|
||
// Define component props
|
||
const props = defineProps({
|
||
tabName: {
|
||
type: String,
|
||
default: ''
|
||
},
|
||
fieldList: {
|
||
type: Array,
|
||
default: () => []
|
||
},
|
||
conf: {
|
||
type: Object,
|
||
default: () => ({})
|
||
}
|
||
})
|
||
|
||
// Define refs for reactive data and component references
|
||
const lf = ref(null) // Reference to LogicFlow instance
|
||
const activeEdges = ref([]) // Reactive array for active edges
|
||
const diagramRef = ref(null) // Reference to the diagram container
|
||
const PropertyPanelRef = ref(null) // Reference to the PropertyPanel component
|
||
|
||
// Lifecycle hook to initialize LogicFlow when the component is mounted
|
||
onMounted(() => {
|
||
initLogicFlow(props.conf)
|
||
})
|
||
|
||
// Function to initialize LogicFlow
|
||
function initLogicFlow(data) {
|
||
// 引入框选插件
|
||
LogicFlow.use(SelectionSelect)
|
||
LogicFlow.use(Menu)
|
||
LogicFlow.use(BpmnElement)
|
||
|
||
// Creating a new LogicFlow instance
|
||
const logicFlowInstance = new LogicFlow({
|
||
container: diagramRef.value, // Setting the container where LogicFlow will be rendered
|
||
overlapMode: 1,
|
||
autoWrap: true,
|
||
metaKeyMultipleSelected: true,
|
||
keyboard: {
|
||
enabled: true
|
||
},
|
||
grid: {
|
||
size: 10,
|
||
type: 'dot'
|
||
}
|
||
})
|
||
|
||
// Setting theme for LogicFlow
|
||
logicFlowInstance.setTheme({
|
||
baseEdge: { strokeWidth: 1 },
|
||
baseNode: { strokeWidth: 1 },
|
||
nodeText: { overflowMode: 'autoWrap', lineHeight: 1.5 },
|
||
edgeText: { overflowMode: 'autoWrap', lineHeight: 1.5 }
|
||
})
|
||
|
||
// Registering custom elements for LogicFlow
|
||
registerCustomElement(logicFlowInstance)
|
||
|
||
// Setting default edge type and rendering initial data
|
||
logicFlowInstance.setDefaultEdgeType('pro-polyline')
|
||
logicFlowInstance.render(data)
|
||
|
||
// Assigning the LogicFlow instance to the 'lf' ref
|
||
lf.value = logicFlowInstance
|
||
|
||
// Event listener for node clicks
|
||
lf.value.on('node:click', (e) => {
|
||
console.log('Click on node', e.data)
|
||
PropertyPanelRef.value.open(e.data, props.fieldList)
|
||
})
|
||
}
|
||
|
||
// Function to handle dragging nodes into the diagram
|
||
function dragInNode(type, text = '') {
|
||
lf.value.dnd.startDrag({
|
||
type,
|
||
text
|
||
})
|
||
}
|
||
|
||
// Function to set properties of a node
|
||
function setProperties(node, item) {
|
||
lf.value.setProperties(node.id, item)
|
||
}
|
||
// function setZIndex(node, type) {
|
||
// lf.value.setElementZIndex(node.id, type)
|
||
// }
|
||
// Function to import data into the LogicFlow instance
|
||
function importData(text) {
|
||
lf.value.renderRawData(text)
|
||
}
|
||
|
||
// Function to save the graph data
|
||
function saveGraph() {
|
||
const data = lf.value.getGraphData()
|
||
download('export.json', JSON.stringify(data))
|
||
}
|
||
|
||
// Function to download the graph data as a file
|
||
function download(filename, text) {
|
||
window.sessionStorage.setItem(filename, text)
|
||
const element = document.createElement('a')
|
||
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text))
|
||
element.setAttribute('download', filename)
|
||
element.style.display = 'none'
|
||
document.body.appendChild(element)
|
||
element.click()
|
||
document.body.removeChild(element)
|
||
}
|
||
async function getData() {
|
||
/**
|
||
* 校验目标
|
||
* 1. 必须存在开始节点和结束节点
|
||
* 2. 连线方向正确
|
||
* 3. 多余的节点(不重要)
|
||
* 4. 所有分支必须结束节点
|
||
* 5. 检查审批节点设置情况
|
||
* 6. 一个节点可以有多个子网关,子网关只能通过一个,不能有多个普通子节点
|
||
*
|
||
*/
|
||
return new Promise<{
|
||
formData: any
|
||
treeToList: any
|
||
}>((resolve, reject) => {
|
||
const data = lf.value.getGraphData()
|
||
const nodes = data.nodes
|
||
const edges = data.edges
|
||
|
||
let haveMoreChildNode = false
|
||
const sourceNodeIdSum = {} // Node ID -> child nodes mapping
|
||
|
||
edges.forEach((edge) => {
|
||
const targetNode = nodes.find((item) => item.id === edge.targetNodeId)
|
||
if (sourceNodeIdSum[edge.sourceNodeId]) {
|
||
sourceNodeIdSum[edge.sourceNodeId].push(targetNode)
|
||
for (const n of sourceNodeIdSum[edge.sourceNodeId]) {
|
||
if (n.type !== 'bpmn:exclusiveGateway') {
|
||
haveMoreChildNode = true
|
||
break
|
||
}
|
||
}
|
||
} else {
|
||
sourceNodeIdSum[edge.sourceNodeId] = [targetNode]
|
||
}
|
||
})
|
||
|
||
if (haveMoreChildNode) {
|
||
return reject({
|
||
target: props.tabName,
|
||
message: '流程设计-一个节点只能有一个子节点,可以有多个网关'
|
||
})
|
||
}
|
||
// 检查开始节点和结束节点是否存在
|
||
const findStartNode = nodes.filter((item) => item.type === 'bpmn:startEvent')
|
||
const findEndNode = nodes.filter((item) => item.type === 'bpmn:endEvent')
|
||
if (findStartNode.length !== 1 || findEndNode.length !== 1) {
|
||
return reject({
|
||
target: props.tabName,
|
||
message: '流程设计-流程必须有且只有一个开始节点和结束节点'
|
||
})
|
||
}
|
||
|
||
const TreeNode = handel(nodes, 0)
|
||
|
||
function handel(arr, pid) {
|
||
const newArr = []
|
||
arr.forEach((node) => {
|
||
const newNode = {
|
||
id: node.id,
|
||
pid: pid,
|
||
label: node?.text?.value,
|
||
type: node.type,
|
||
fieldAuth: node?.properties?.fieldAuth,
|
||
gateway: node?.properties?.gateway,
|
||
|
||
userType: node?.properties?.userType || 0,
|
||
userId: node?.properties?.userId || 0,
|
||
deptId: node?.properties?.deptId || 0,
|
||
postId: node?.properties?.postId || 0,
|
||
children: null
|
||
}
|
||
if (sourceNodeIdSum[node.id]) {
|
||
newNode.children = handel(sourceNodeIdSum[node.id], node.id)
|
||
}
|
||
newArr.push(newNode)
|
||
})
|
||
return newArr
|
||
}
|
||
|
||
function treeToList(tree) {
|
||
const arr = []
|
||
tree.forEach((item) => {
|
||
arr.push(item)
|
||
if (item.children) {
|
||
arr.push(...treeToList(item.children))
|
||
}
|
||
})
|
||
return arr
|
||
}
|
||
// 检查连线方向是否正确;
|
||
resolve({ formData: data, treeToList: treeToList(TreeNode) })
|
||
})
|
||
}
|
||
|
||
defineExpose({
|
||
getData
|
||
})
|
||
</script>
|
||
|
||
<style lang="scss">
|
||
.diagram {
|
||
width: 100%;
|
||
height: 100%;
|
||
position: relative;
|
||
}
|
||
|
||
.diagram-toolbar {
|
||
position: absolute;
|
||
top: 10px;
|
||
right: 20px;
|
||
height: 40px;
|
||
padding: 0 10px;
|
||
/* width: 310px; */
|
||
display: flex;
|
||
align-items: center;
|
||
/* border-bottom: 1px solid #e5e5e5; */
|
||
z-index: 10;
|
||
background: #fff;
|
||
box-shadow: 0px 0px 4px rgba($color: #000000, $alpha: 0.5);
|
||
}
|
||
.diagram-main {
|
||
position: relative;
|
||
width: 100%;
|
||
height: 100%;
|
||
overflow: hidden;
|
||
|
||
.diagram-sidebar {
|
||
position: absolute;
|
||
left: 20px;
|
||
z-index: 10;
|
||
top: 50px;
|
||
width: 60px;
|
||
background: #fff;
|
||
box-shadow: 0px 0px 4px rgba($color: #000000, $alpha: 0.5);
|
||
}
|
||
.diagram-container {
|
||
height: 100%;
|
||
.diagram-wrapper {
|
||
box-sizing: border-box;
|
||
width: 100%;
|
||
height: 100%;
|
||
|
||
.lf-diagram {
|
||
// box-shadow: 0px 0px 4px #838284;
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
}
|
||
}
|
||
/* 由于背景图和gird不对齐,需要css处理一下 */
|
||
.diagram-container :v-deep .lf-background {
|
||
left: -9px;
|
||
}
|
||
}
|
||
|
||
::-webkit-scrollbar {
|
||
width: 9px;
|
||
height: 9px;
|
||
background: white;
|
||
border-left: 1px solid #e8e8e8;
|
||
}
|
||
::-webkit-scrollbar-thumb {
|
||
border-width: 1px;
|
||
border-style: solid;
|
||
border-color: #fff;
|
||
border-radius: 6px;
|
||
background: #c9c9c9;
|
||
}
|
||
::-webkit-scrollbar-thumb:hover {
|
||
background: #b5b5b5;
|
||
}
|
||
</style>
|