2025-09-03 09:16:13 +08:00
|
|
|
<template>
|
|
|
|
|
<div class="page">
|
|
|
|
|
<el-container class="page__container">
|
|
|
|
|
<el-header v-if="!flowInfoData.isLog" >
|
|
|
|
|
<div>
|
|
|
|
|
<div>{{ flowInfoData.name }}</div>
|
|
|
|
|
<div>{{ stateMap[flowState] }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="btn__container">
|
|
|
|
|
<div class="btn">
|
|
|
|
|
<el-button @click="group">分组</el-button>
|
|
|
|
|
<el-button v-if="flowState !== 'testRunning'" @click="testRun">试运行</el-button>
|
|
|
|
|
<el-button v-if="flowState === 'testRunning'" @click="flowPauseFn">暂停</el-button>
|
|
|
|
|
<el-button v-if="flowState === 'pause'" @click="flowResumeFn">恢复</el-button>
|
|
|
|
|
<el-button v-if="['testRunning', 'pause'].includes(flowState)" @click="flowStopFn">终止</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="btn">
|
|
|
|
|
<el-tooltip
|
|
|
|
|
class="box-item"
|
|
|
|
|
effect="dark"
|
|
|
|
|
content="试运行成功后才可以发布"
|
|
|
|
|
placement="top"
|
|
|
|
|
v-if="flowState !== 'testRunFinish'"
|
|
|
|
|
>
|
|
|
|
|
<el-button type="primary" :disabled="true">发布</el-button>
|
|
|
|
|
</el-tooltip>
|
|
|
|
|
<el-button v-else @click="deployFlow" type="primary">发布</el-button>
|
|
|
|
|
|
|
|
|
|
<el-popover :width="260" @show="showNodeRelationship" popper-class="node-relationship-network-popover">
|
|
|
|
|
<template #reference>
|
|
|
|
|
<el-button style="margin-right: 16px"><el-icon><Location /></el-icon></el-button>
|
|
|
|
|
</template>
|
|
|
|
|
<div>
|
|
|
|
|
<div class="title">节点关系网</div>
|
|
|
|
|
<div class="fit">
|
|
|
|
|
<el-tooltip
|
|
|
|
|
class="box-item"
|
|
|
|
|
effect="dark"
|
|
|
|
|
content="适应屏幕"
|
|
|
|
|
placement="bottom"
|
|
|
|
|
>
|
|
|
|
|
<el-button size="small" circle @click="fitView"><el-icon><Location /></el-icon></el-button>
|
|
|
|
|
</el-tooltip>
|
|
|
|
|
</div>
|
|
|
|
|
<el-tree
|
|
|
|
|
style="max-width: 600px"
|
|
|
|
|
:data="treeData"
|
|
|
|
|
:props="defaultProps"
|
|
|
|
|
:default-expand-all="true"
|
|
|
|
|
@node-click="handleTreeNodeClick"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</el-popover>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</el-header>
|
2025-11-17 14:54:32 +08:00
|
|
|
<el-container class="container">
|
2025-09-03 09:16:13 +08:00
|
|
|
<el-aside v-if="!flowStore.disableForm">
|
|
|
|
|
<Aside />
|
|
|
|
|
</el-aside>
|
|
|
|
|
<el-main>
|
|
|
|
|
<div class="flow__container" ref="flowContainerRef" @dragover="dragover" @drop="drop"></div>
|
|
|
|
|
</el-main>
|
|
|
|
|
</el-container>
|
|
|
|
|
</el-container>
|
|
|
|
|
|
|
|
|
|
<TestRun :drawer="isOpen" @close="isOpen = false" @changeState="changeState" :flowId="flowInfoData.itemId" />
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
<script setup>
|
|
|
|
|
import { ref, onMounted, onUnmounted, nextTick } from "vue";
|
|
|
|
|
import LogicFlow, { action } from "@logicflow/core";
|
|
|
|
|
import "@logicflow/core/es/index.css";
|
|
|
|
|
import "@logicflow/core/lib/style/index.css";
|
|
|
|
|
import '@logicflow/extension/lib/style/index.css'
|
|
|
|
|
import { lfConfig, registerCustomizeNode } from './config'
|
|
|
|
|
import TestRun from "./components/TestRun.vue";
|
|
|
|
|
import Aside from "./components/Aside.vue";
|
|
|
|
|
import { ElMessage } from 'element-plus'
|
|
|
|
|
import { useFlowStore } from '@/store/modules/flow'
|
|
|
|
|
import { emitter } from '@/utils/eventBus';
|
2025-11-17 14:54:32 +08:00
|
|
|
import { recursiveFilter, newEdge, convertToTree } from '@/utils/flow'
|
2025-09-03 09:16:13 +08:00
|
|
|
import { flowDeploy, flowView, flowPause, flowResume, flowStop } from '@/api/device/flow'
|
|
|
|
|
import { getDetect } from '@/api/test/detect'
|
|
|
|
|
import { Location } from "@element-plus/icons-vue";
|
|
|
|
|
|
|
|
|
|
const flowContainerRef = ref(null);
|
|
|
|
|
const lfRef = ref(null);
|
|
|
|
|
|
|
|
|
|
const dragover = (e) => {
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const drop = (e) => {
|
|
|
|
|
let node = e.dataTransfer.getData('flowNode');
|
|
|
|
|
node = JSON.parse(node)
|
|
|
|
|
if (['stopLoop', 'subEnd', 'subStart'].includes(node.type)) {
|
|
|
|
|
ElMessage.warning('此节点只能拖拽到循环节点的循环体中')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
const point = lf.getPointByClient(e.clientX, e.clientY)
|
|
|
|
|
const { type, ...other } = node
|
|
|
|
|
lf.addNode({
|
|
|
|
|
type,
|
|
|
|
|
x: point.canvasOverlayPosition.x,
|
|
|
|
|
y: point.canvasOverlayPosition.y,
|
|
|
|
|
properties: {
|
|
|
|
|
...other
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const flowStore = useFlowStore()
|
|
|
|
|
flowStore.getDeviceList()
|
|
|
|
|
|
|
|
|
|
const isOpen = ref(false)
|
|
|
|
|
const stateMap = {
|
|
|
|
|
unpublished: '未发布',
|
|
|
|
|
testRunning: '试运行中',
|
|
|
|
|
testRunFinish: '试运行成功',
|
|
|
|
|
testRunError: '试运行失败',
|
|
|
|
|
release: '已发布',
|
|
|
|
|
updateData: '修改中',
|
|
|
|
|
pause: '已暂停',
|
|
|
|
|
stop: '已终止'
|
|
|
|
|
}
|
|
|
|
|
const flowState = ref('unpublished')
|
|
|
|
|
const flowInfoData = ref({
|
|
|
|
|
itemId: '',
|
|
|
|
|
instId: '',
|
|
|
|
|
taskId: '',
|
|
|
|
|
isLog: false,
|
|
|
|
|
name: '流程'
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 试运行
|
|
|
|
|
*/
|
|
|
|
|
const testRun = async () => {
|
|
|
|
|
// 1、验证画布内所有的表单通过验证
|
|
|
|
|
const { nodes, edges } = lf.getGraphData()
|
|
|
|
|
|
|
|
|
|
const isolatedNodes = nodes.filter(node => {
|
|
|
|
|
const nodeId = node.id;
|
|
|
|
|
// 检查是否存在关联边
|
|
|
|
|
return !edges.some(edge =>
|
|
|
|
|
edge.sourceNodeId === nodeId || edge.targetNodeId === nodeId
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const gl = isolatedNodes.filter(item => {
|
|
|
|
|
return !item.properties?.parentId && item.type !== 'selectArea'
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (gl && gl.length > 0) {
|
|
|
|
|
ElMessage.warning('存在孤立的节点,请检查!')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let allValid = true;
|
|
|
|
|
for (const node of nodes) {
|
|
|
|
|
const nodeModel = lf.getNodeModelById(node.id);
|
|
|
|
|
if (nodeModel && nodeModel.validateForm) {
|
|
|
|
|
const result = await nodeModel.validateForm();
|
|
|
|
|
if (!result.valid) {
|
|
|
|
|
allValid = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (allValid) {
|
|
|
|
|
isOpen.value = true
|
|
|
|
|
} else {
|
|
|
|
|
ElMessage.error('部分节点验证失败,请查看详情');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 流程暂停
|
|
|
|
|
const flowPauseFn = async () => {
|
|
|
|
|
const res = await flowPause(instId.value)
|
|
|
|
|
if (res.code === 200) {
|
|
|
|
|
flowState.value = 'pause'
|
|
|
|
|
ElMessage.success(res.msg);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const flowResumeFn = async () => {
|
|
|
|
|
const res = await flowResume(instId.value)
|
|
|
|
|
if (res.code === 200) {
|
|
|
|
|
flowState.value = 'testRunning'
|
|
|
|
|
loopFlowView(instId.value)
|
|
|
|
|
ElMessage.success(res.msg);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const flowStopFn = async () => {
|
|
|
|
|
const res = await flowStop(instId.value)
|
|
|
|
|
if (res.code === 200) {
|
|
|
|
|
flowState.value = 'stop'
|
|
|
|
|
flowStore.updateDisableForm(false)
|
|
|
|
|
ElMessage.success(res.msg);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const initFlowData = {
|
|
|
|
|
nodes: [
|
|
|
|
|
{
|
|
|
|
|
type: "start",
|
|
|
|
|
x: 300,
|
|
|
|
|
y: 100,
|
|
|
|
|
properties: {
|
|
|
|
|
name: 'Start',
|
|
|
|
|
action: 'start'
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
type: "end",
|
|
|
|
|
x: 1300,
|
|
|
|
|
y: 700,
|
|
|
|
|
properties: {
|
|
|
|
|
name: 'End',
|
|
|
|
|
action: 'end'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
// 边
|
|
|
|
|
edges: [],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const validEdge = (data) => {
|
|
|
|
|
const sourceAnchorArr = lf.getNodeModelById(data.sourceNodeId).anchors
|
|
|
|
|
const [sourceName] = sourceAnchorArr.filter(item => {
|
|
|
|
|
return item.id === data.sourceAnchorId
|
|
|
|
|
}).map(item=> {
|
|
|
|
|
return item.name
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const targetAnchorArr = lf.getNodeModelById(data.targetNodeId).anchors
|
|
|
|
|
const [targetName] = targetAnchorArr.filter(item => {
|
|
|
|
|
return item.id === data.targetAnchorId
|
|
|
|
|
}).map(item=> {
|
|
|
|
|
return item.name
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (sourceName === 'right' && targetName === 'left') {
|
|
|
|
|
return
|
|
|
|
|
} else if ((sourceName === 'right' && targetName === 'right') || (sourceName === 'left' && targetName === 'left')) {
|
|
|
|
|
lf.deleteEdge(data.id);
|
|
|
|
|
} else if (sourceName === 'left' && targetName === 'right') {
|
|
|
|
|
const sourceNodeId = data.targetNodeId
|
|
|
|
|
const sourceAnchorId = data.targetAnchorId
|
|
|
|
|
const targetNodeId = data.sourceNodeId
|
|
|
|
|
const targetAnchorId = data.sourceAnchorId
|
|
|
|
|
lf.deleteEdge(data.id);
|
|
|
|
|
lf.addEdge({
|
|
|
|
|
type: 'bezier',
|
|
|
|
|
sourceNodeId,
|
|
|
|
|
targetNodeId,
|
|
|
|
|
sourceAnchorId,
|
|
|
|
|
targetAnchorId
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const instId = ref(null)
|
|
|
|
|
const changeState = (value) => {
|
|
|
|
|
const flowData = JSON.parse(JSON.stringify(lf.getGraphData()))
|
|
|
|
|
oldFlowData = flowData
|
|
|
|
|
isOpen.value = false
|
|
|
|
|
flowStore.updateDisableForm(true)
|
|
|
|
|
flowState.value = value.type
|
|
|
|
|
|
|
|
|
|
instId.value = value.instId
|
|
|
|
|
loopFlowView(value.instId)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const loopFlowView = async (instId, taskId = null) => {
|
|
|
|
|
const params = {
|
|
|
|
|
itemId: flowInfoData.value.itemId,
|
|
|
|
|
instId,
|
|
|
|
|
}
|
|
|
|
|
if (taskId) {
|
|
|
|
|
params.taskId = taskId
|
|
|
|
|
}
|
|
|
|
|
const res = await flowView(params)
|
|
|
|
|
if (res.code === 200) {
|
|
|
|
|
const arr = res.data || []
|
|
|
|
|
arr.forEach(item => {
|
|
|
|
|
emitter.emit('changeNodeState', item)
|
|
|
|
|
})
|
|
|
|
|
oldFlowData = JSON.parse(JSON.stringify(lf.getGraphData()))
|
|
|
|
|
if (arr[arr.length - 1]?.nodeType !== 'END' && !['FAILED', 'STOPPED'].includes(arr[arr.length - 1]?.status)) {
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
loopFlowView(instId)
|
|
|
|
|
}, 2000)
|
|
|
|
|
} else if (arr[arr.length - 1]?.nodeType === 'END' && arr[arr.length - 1]?.status === 'SUCCESS') {
|
|
|
|
|
if (!flowInfoData.value.isLog) {
|
|
|
|
|
flowStore.updateDisableForm(false)
|
|
|
|
|
flowState.value = 'testRunFinish'
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
if (!flowInfoData.value.isLog) {
|
|
|
|
|
if (['PAUSED', 'STOPPED'].includes(arr[arr.length - 1]?.status)) {
|
|
|
|
|
flowState.value = arr[arr.length - 1]?.status === 'PAUSED' ? 'pause' : 'stop'
|
|
|
|
|
} else {
|
|
|
|
|
flowState.value = 'testRunError'
|
|
|
|
|
flowStore.updateDisableForm(false)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let oldFlowData = {}
|
|
|
|
|
/**
|
|
|
|
|
* 发布
|
|
|
|
|
*/
|
|
|
|
|
const deployFlow = async () => {
|
|
|
|
|
if (flowState.value !== 'testRunFinish') {
|
|
|
|
|
ElMessage.warning('试运行成功后才可以发布')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const res = await flowDeploy({ id: flowInfoData.value.itemId })
|
|
|
|
|
|
|
|
|
|
if (res.code === 200) {
|
|
|
|
|
ElMessage.success(res.msg)
|
|
|
|
|
flowState.value = 'release'
|
|
|
|
|
} else {
|
|
|
|
|
ElMessage.error(res.msg)
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
ElMessage.error('部署操作产生异常')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const loadFlowData = async (id, lf) => {
|
|
|
|
|
try {
|
|
|
|
|
const res = await getDetect(id)
|
|
|
|
|
if (res.data && res.data.flowData) {
|
|
|
|
|
const flowData = JSON.parse(res.data.flowData)
|
|
|
|
|
const selectArea = []
|
|
|
|
|
flowData.nodes.forEach(item => {
|
|
|
|
|
if(item.type === 'selectArea') {
|
|
|
|
|
selectArea.push(...item.children)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
lf.render(flowData);
|
|
|
|
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
selectArea.forEach(item => {
|
|
|
|
|
lf.getNodeModelById(item).draggable = false
|
|
|
|
|
})
|
|
|
|
|
oldFlowData = JSON.parse(JSON.stringify(lf.getGraphData()))
|
|
|
|
|
lf.fitView()
|
|
|
|
|
})
|
|
|
|
|
} else {
|
|
|
|
|
// 渲染数据 data
|
|
|
|
|
lf.render(initFlowData);
|
|
|
|
|
oldFlowData = initFlowData
|
|
|
|
|
nextTick(() => lf.zoom(0.8, [850, 450]) )
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch(err) {
|
|
|
|
|
lf.render(initFlowData);
|
|
|
|
|
oldFlowData = initFlowData
|
|
|
|
|
nextTick(() => lf.zoom(0.8, [850, 450]) )
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const registerBeforeUnload = () => {
|
|
|
|
|
window.addEventListener('beforeunload', handleBeforeUnload)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleBeforeUnload = (event) => {
|
|
|
|
|
if (flowState.value === 'pause' && !flowInfoData.value.isLog) {
|
|
|
|
|
flowStopFn()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const justLoadFlow = () => {
|
|
|
|
|
const url = new URL(window.location.href);
|
|
|
|
|
const locationParams = Object.fromEntries(url.searchParams.entries())
|
|
|
|
|
if (locationParams?.itemId) {
|
|
|
|
|
flowState.value = locationParams.state === '0' ? 'release' : 'unpublished'
|
|
|
|
|
flowInfoData.value = {
|
|
|
|
|
itemId: locationParams.itemId,
|
|
|
|
|
instId: locationParams?.instId || '',
|
|
|
|
|
taskId: locationParams?.taskId || '',
|
|
|
|
|
isLog: locationParams?.isLog || false,
|
|
|
|
|
name: decodeURIComponent(locationParams.name)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (flowInfoData.value.isLog) {
|
|
|
|
|
flowStore.updateDisableForm(true)
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
loopFlowView(flowInfoData.value.instId, flowInfoData.value.taskId)
|
|
|
|
|
}, 1000)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const initFlow = () => {
|
|
|
|
|
if (flowContainerRef.value) {
|
|
|
|
|
const lf = new LogicFlow({
|
|
|
|
|
container: flowContainerRef.value,
|
|
|
|
|
...lfConfig,
|
|
|
|
|
zoomOptions: {
|
|
|
|
|
initScale: 0.5, // 初始缩放 50%
|
|
|
|
|
minScale: 0.1, // 最小缩放 10%
|
|
|
|
|
maxScale: 3 // 最大缩放 300%
|
|
|
|
|
},
|
|
|
|
|
keyboard: {
|
|
|
|
|
enabled: true,
|
|
|
|
|
shortcuts: [
|
|
|
|
|
{
|
|
|
|
|
keys: ["backspace", "delete"], // 覆盖删除键
|
|
|
|
|
callback: () => {
|
|
|
|
|
const activeElem = document.activeElement;
|
|
|
|
|
if (activeElem.tagName === 'INPUT' ||
|
|
|
|
|
activeElem.tagName === 'TEXTAREA') {
|
|
|
|
|
return; // 不处理输入框内的删除
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const { nodes, edges } = lf.getSelectElements();
|
|
|
|
|
if (nodes.length > 0) {
|
|
|
|
|
// 检查是否存在受保护节点
|
|
|
|
|
const hasProtectedNode = nodes.some(node =>
|
|
|
|
|
['start', 'end'].includes(node.type)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if (hasProtectedNode) {
|
|
|
|
|
lf.selectElementById(null); // 取消选中
|
|
|
|
|
return; // 阻断删除
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 默认删除逻辑
|
|
|
|
|
nodes.forEach(node => {
|
|
|
|
|
if (node.type === 'selectArea') {
|
|
|
|
|
lf.getNodeModelById(node.id).children = []
|
|
|
|
|
node.children.forEach(item => {
|
|
|
|
|
lf.getNodeModelById(item).draggable = true
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
lf.deleteNode(node.id)
|
|
|
|
|
// 删除节点所有的边
|
|
|
|
|
lf.deleteEdgeByNodeId(node.id);
|
|
|
|
|
});
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if (nodes.length ===0 && edges.length > 0) {
|
|
|
|
|
edges.forEach(node => {
|
|
|
|
|
// 删除边
|
|
|
|
|
lf.deleteEdge(node.id);
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
keys: ["ctrl+z"], // 自定义快捷键
|
|
|
|
|
callback: () => {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
keys: ["ctrl+shift+z"],
|
|
|
|
|
callback: () => false
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
registerCustomizeNode(lf)
|
|
|
|
|
|
|
|
|
|
loadFlowData(flowInfoData.value.itemId, lf)
|
|
|
|
|
|
|
|
|
|
lf.on('edge:add', ({ data }) => {
|
|
|
|
|
validEdge(data)
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 监听历史记录变化
|
|
|
|
|
lf.on("history:change", ({ data }) => {
|
|
|
|
|
if (flowStore.disableForm) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
const flowData = lf.getGraphData()
|
|
|
|
|
if (JSON.stringify(oldFlowData) === JSON.stringify(flowData)) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
flowState.value = 'updateData'
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 监听子节点拖动事件
|
|
|
|
|
lf.on("node:mousemove", ({ data, e }) => {
|
|
|
|
|
const { nodes } = lf.getGraphData();
|
|
|
|
|
const arr = recursiveFilter(nodes, data.id, 'loop')
|
|
|
|
|
arr.forEach(item => {
|
|
|
|
|
const node = lf.getNodeModelById(item.id)
|
|
|
|
|
node.updateSize()
|
|
|
|
|
})
|
|
|
|
|
operationEdge(arr)
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
lf.on("node:mouseleave", ({ data }) => {
|
|
|
|
|
const node = lf.getNodeModelById(data.id)
|
|
|
|
|
node.setCustomProperties && node.setCustomProperties()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
lfRef.value = lf;
|
|
|
|
|
window.lf = lf
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const group = () => {
|
|
|
|
|
const PADDING = 200
|
|
|
|
|
lf.extension.selectionSelect.openSelectionSelect();
|
|
|
|
|
lf.once('selection:selected', (data) => {
|
|
|
|
|
|
|
|
|
|
let minX = Infinity
|
|
|
|
|
let minY = Infinity
|
|
|
|
|
let maxX = -Infinity
|
|
|
|
|
let maxY = -Infinity
|
|
|
|
|
|
|
|
|
|
let children = []
|
|
|
|
|
data.elements.forEach(item => {
|
|
|
|
|
if (item.BaseType === 'node') {
|
|
|
|
|
minX = Math.min(minX, item.x - item.width / 2)
|
|
|
|
|
minY = Math.min(minY, item.y - item.height / 2)
|
|
|
|
|
maxX = Math.max(maxX, item.x + item.width / 2)
|
|
|
|
|
maxY = Math.max(maxY, item.y + item.height / 2)
|
|
|
|
|
children.push(item.id)
|
|
|
|
|
lf.getNodeModelById(item.id).draggable = false
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
if (children.length === 0) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
let width = maxX - minX + PADDING
|
|
|
|
|
let height = maxY - minY + PADDING
|
|
|
|
|
|
|
|
|
|
lf.addNode({
|
|
|
|
|
type: 'selectArea',
|
|
|
|
|
x: minX + (maxX - minX) / 2,
|
|
|
|
|
y: minY + (maxY - minY) / 2,
|
|
|
|
|
properties: {
|
|
|
|
|
width,
|
|
|
|
|
height,
|
|
|
|
|
},
|
|
|
|
|
children
|
|
|
|
|
})
|
|
|
|
|
lf.clearSelectElements()
|
|
|
|
|
lf.extension.selectionSelect.closeSelectionSelect();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const treeData = ref(null)
|
|
|
|
|
const defaultProps = {
|
|
|
|
|
children: 'children',
|
|
|
|
|
label: 'name',
|
|
|
|
|
}
|
|
|
|
|
const showNodeRelationship = () => {
|
|
|
|
|
const { nodes } = lf.getGraphData()
|
|
|
|
|
const outputData = convertToTree(nodes);
|
|
|
|
|
treeData.value = outputData
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleTreeNodeClick = (node) => {
|
|
|
|
|
lf.focusOn({
|
|
|
|
|
id: node.id
|
|
|
|
|
})
|
2025-11-17 15:24:45 +08:00
|
|
|
lf.selectElementById(node.id)
|
2025-09-03 09:16:13 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const fitView = () => {
|
|
|
|
|
lf.fitView()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
justLoadFlow()
|
|
|
|
|
initFlow()
|
|
|
|
|
|
|
|
|
|
registerBeforeUnload()
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const operationEdge = (arr) => {
|
|
|
|
|
arr.forEach(item => {
|
2025-11-17 14:54:32 +08:00
|
|
|
newEdge(item.id)
|
2025-09-03 09:16:13 +08:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
emitter.off('changeNodeState');
|
|
|
|
|
window.removeEventListener('beforeunload', handleBeforeUnload)
|
|
|
|
|
})
|
|
|
|
|
</script>
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
|
.page {
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
2025-11-17 14:54:32 +08:00
|
|
|
overflow: hidden;
|
2025-09-03 09:16:13 +08:00
|
|
|
|
|
|
|
|
.page__container {
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
position: relative;
|
|
|
|
|
|
|
|
|
|
.el-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
border-bottom: 1px solid #ddd;
|
|
|
|
|
|
|
|
|
|
.btn__container {
|
|
|
|
|
display: flex;
|
|
|
|
|
|
|
|
|
|
.btn {
|
|
|
|
|
margin-right: 12px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-17 14:54:32 +08:00
|
|
|
.container {
|
|
|
|
|
height: calc(100% - 60px);
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-03 09:16:13 +08:00
|
|
|
.el-main {
|
|
|
|
|
padding: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.el-aside {
|
|
|
|
|
margin: 0;
|
|
|
|
|
width: 260px;
|
|
|
|
|
padding: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.flow__container {
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
|
|
|
|
:deep(.lf-base) {
|
|
|
|
|
foreignObject {
|
|
|
|
|
overflow: visible !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.lf-node-content {
|
|
|
|
|
|
|
|
|
|
.lf-node-anchor {
|
|
|
|
|
display: block !important; /* 强制显示 */
|
|
|
|
|
opacity: 1 !important; /* 取消透明度 */
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.lf-node-selected {
|
|
|
|
|
.node__box {
|
|
|
|
|
border-color: #8dc99d;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.node__container {
|
|
|
|
|
border-color: #8dc99d;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.group__container {
|
|
|
|
|
border-color: #8dc99d;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
<style lang="scss">
|
|
|
|
|
.node-relationship-network-popover {
|
|
|
|
|
position: relative;
|
|
|
|
|
|
|
|
|
|
.title {
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
text-align: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.fit {
|
|
|
|
|
position: absolute;
|
|
|
|
|
right: 20px;
|
|
|
|
|
top: 10px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.el-tree {
|
|
|
|
|
margin-top: 20px;
|
|
|
|
|
max-height: 200px;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|