CMVR-IOT-UI/src/views/flow/index.vue

693 lines
18 KiB
Vue
Raw Normal View History

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>
<el-container>
<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';
import { recursiveFilter, addNewEdge, convertToTree } from '@/utils/flow'
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
})
}
const fitView = () => {
lf.fitView()
}
onMounted(() => {
justLoadFlow()
initFlow()
registerBeforeUnload()
});
const operationEdge = (arr) => {
arr.forEach(item => {
addNewEdge(item.id)
})
}
onUnmounted(() => {
emitter.off('changeNodeState');
window.removeEventListener('beforeunload', handleBeforeUnload)
})
</script>
<style lang="scss" scoped>
.page {
width: 100%;
height: 100%;
.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;
}
}
}
.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>