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

911 lines
24 KiB
Vue

<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-tag style="margin-right: 12px;" v-if="['testRunError', 'testRunFinish'].includes(flowState)" :type="flowState === 'testRunFinish' ? 'success' : 'danger'">
{{ totalRunningTime }}ms
</el-tag>
<el-button v-if="['testRunError', 'testRunFinish', 'stop'].includes(flowState)" @click="clearRun">清除上一次运行结果</el-button>
<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 class="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"
/>
<ParamsDrawer
:drawer="showParamsDrawer"
:data="paramsDrawerData"
@close="showParamsDrawer = false"
/>
</div>
<el-dialog v-model="visible" width="500" append-to-body>
<template #header="{ close, titleId, titleClass }">
<div class="my-header">
<h4 :id="titleId" :class="titleClass">执行{{ nodeName }}节点</h4>
</div>
</template>
<el-form
:inline="true"
:model="formData"
:rules="rules"
ref="executeForm"
label-position="top"
label-width="auto"
:disabled="flowStore.disableForm"
>
<div v-for="(property, index) in formData.nodeParams" :key="index">
<el-row>
<el-form-item
:label="index === 0 ? '参数名' : ''"
:prop="`nodeParams.${index}.name`"
:rules="[{ required: true, message: '请输入参数名', trigger: 'blur' }]"
>
<el-input
:disabled="property?.disabled || false"
v-model="property.name"
placeholder="请输入"
clearable
/>
</el-form-item>
<el-form-item
:label="index === 0 ? '参数值' : ''"
:prop="`nodeParams.${index}.input`"
:rules="[{ required: true, message: '请输入参数值', trigger: 'blur' }]"
>
<el-input-number
v-if="property.componentType === 'number'"
v-model="property.input"
:min="0"
:controls="false"
:step-strictly="true"
placeholder="请输入"
clearable
/>
<el-select
v-model="property.input"
v-else-if="property.componentType === 'select'"
>
<el-option
v-for="item in property.selectOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<el-input
v-else
v-model="property.input"
autosize
type="textarea"
placeholder="请输入"
clearable
/>
</el-form-item>
</el-row>
</div>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="execute"> 确定 </el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { ref, onMounted, onUnmounted, nextTick } from "vue";
import LogicFlow 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 ParamsDrawer from "./components/ParamsDrawer.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, newEdge, convertToTree } from "@/utils/flow";
import {
flowDeploy,
flowView,
flowPause,
flowResume,
flowStop,
flowAction,
} 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", "currentLoop"].includes(node.type)) {
ElMessage.warning("此节点只能拖拽到循环节点的循环体中");
return;
}
const point = lf.getPointByClient(e.clientX, e.clientY);
const { type, ...other } = node;
const newNode = lf.addNode({
type,
x: point.canvasOverlayPosition.x,
y: point.canvasOverlayPosition.y,
properties: {
...other,
},
});
if (
!["selectArea", "branch", "stopLoop", "subStart", "subEnd"].includes(newNode.type)
) {
showParamsDrawer.value = true;
paramsDrawerData.value = newNode;
}
};
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;
}
isOpen.value = true;
};
// 流程暂停
const flowPauseFn = async () => {
const res = await flowPause(instId.value);
if (res.code === 200) {
flowState.value = "pause";
ElMessage.success(res.msg);
}
};
const totalRunningTime = ref(0);
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";
totalRunningTime.value = arr[arr.length - 1].endTime - arr[0].startTime
}
} 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";
totalRunningTime.value = arr[arr.length - 1].endTime - arr[0].startTime
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");
// const arr = findRelationNodesById(nodes, data.id);
console.log('arr', arr)
arr.forEach((item) => {
const node = lf.getNodeModelById(item.id);
node.updateSize();
});
operationEdge(arr);
});
lf.on("node:mouseleave", ({ data }) => {
const node = lf.getNodeModelById(data.id);
if (node.setCustomProperties) {
node.setCustomProperties();
}
});
lf.on("node:dbclick", ({ data, e }) => {
if (
["selectArea", "branch", "stopLoop", "subStart", "subEnd"].includes(data.type)
) {
return;
}
console.log("data", data);
showParamsDrawer.value = true;
paramsDrawerData.value = data;
});
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,
});
lf.selectElementById(node.id);
};
const fitView = () => {
lf.fitView();
};
const nodeName = ref("");
const visible = ref(false);
const formData = reactive({
nodeParams: [],
});
const rules = reactive({});
const executeForm = ref();
const nodeAction = ref("");
const singleNodeExecution = (e) => {
const { name, nodeType, nodeParams, action } = e.detail;
nodeName.value = name;
visible.value = true;
nodeAction.value = action;
if (nodeType === "EDGE") {
formData.nodeParams = [
{
disabled: true,
input: "",
name: "terminalId",
type: "input",
},
...nodeParams,
];
} else {
formData.nodeParams = nodeParams;
}
};
const execute = async () => {
const result = await executeForm.value.validate();
if (result) {
const obj = {};
formData.nodeParams.forEach((item) => {
obj[item.name] = item.input;
});
const res = await flowAction({
action: nodeAction.value,
payload: obj,
});
if (res.code === 200) {
ElMessage.success("执行成功");
} else {
ElMessage.error(res.msg);
}
}
};
const showParamsDrawer = ref(false);
const paramsDrawerData = ref({});
const clearRun = () => {
const { nodes } = lf.getGraphData();
nodes.forEach((item) => {
const node = lf.getNodeModelById(item.id);
if (node.closePopover) {
node.closePopover();
}
});
}
onMounted(() => {
justLoadFlow();
initFlow();
document.addEventListener("singleNodeExecution", singleNodeExecution);
registerBeforeUnload();
emitter.on("openParamsDrawer", (node) => {
showParamsDrawer.value = true;
paramsDrawerData.value = node;
});
});
const operationEdge = (arr) => {
arr.forEach((item) => {
newEdge(item.id);
});
};
onUnmounted(() => {
emitter.off("changeNodeState");
emitter.off("openParamsDrawer");
window.removeEventListener("beforeunload", handleBeforeUnload);
document.removeEventListener("singleNodeExecution", singleNodeExecution);
});
</script>
<style lang="scss" scoped>
.page {
width: 100%;
height: 100%;
overflow: hidden;
.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;
}
}
}
.container {
height: calc(100% - 60px);
}
.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;
}
}
.el-dialog {
.el-input {
width: 200px;
}
.el-select {
width: 200px;
}
.el-textarea {
width: 200px;
}
}
</style>