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

1110 lines
31 KiB
Vue
Raw Normal View History

2026-07-28 16:26:07 +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-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>
2026-07-29 11:43:07 +08:00
<VueFlow
id="main-flow"
2026-07-28 16:26:07 +08:00
class="flow__container"
ref="flowContainerRef"
2026-07-29 11:43:07 +08:00
:min-zoom="0.1"
:max-zoom="3"
:node-drag-threshold="0"
:nodes-draggable="!flowStore.disableForm"
:nodes-connectable="!flowStore.disableForm"
:edges-updatable="!flowStore.disableForm"
:elements-selectable="true"
:delete-key-code="null"
:multi-selection-key-code="['Control', 'Meta']"
:selection-key-code="groupSelectionPending ? true : 'Shift'"
:pan-on-drag="!groupSelectionPending"
:zoom-on-double-click="false"
:default-edge-options="defaultEdgeOptions"
@connect="handleConnect"
@node-double-click="handleNodeDoubleClick"
@node-drag-start="handleNodeDragStart"
@node-drag="handleNodeDrag"
@node-drag-stop="handleNodeDragStop"
@nodes-change="handleNodesChange"
@edges-change="handleEdgesChange"
@edge-click="handleEdgeClick"
@pane-click="handlePaneClick"
@selection-end="handleSelectionEnd"
2026-07-28 16:26:07 +08:00
@dragover="dragover"
@drop="drop"
2026-07-29 11:43:07 +08:00
>
<template #node-flow-node="nodeProps">
<FlowNode v-bind="nodeProps" />
</template>
<template #edge-flow-edge="edgeProps">
<FlowEdge v-bind="edgeProps" />
</template>
<Background color="#d9dee8" :gap="24" :size="1" />
<Controls position="bottom-right" />
</VueFlow>
2026-07-28 16:26:07 +08:00
</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>
2026-07-29 11:43:07 +08:00
import { computed, reactive, ref, onMounted, onUnmounted, nextTick } from "vue";
import { MarkerType, VueFlow, useVueFlow } from "@vue-flow/core";
import { Background } from "@vue-flow/background";
import { Controls } from "@vue-flow/controls";
import "@vue-flow/core/dist/style.css";
import "@vue-flow/core/dist/theme-default.css";
import "@vue-flow/controls/dist/style.css";
import FlowNode from "./vue-flow/FlowNode.vue";
import FlowEdge from "./vue-flow/FlowEdge.vue";
import { createFlowFacade } from "./vue-flow/adapter";
2026-07-28 16:26:07 +08:00
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);
2026-07-29 11:43:07 +08:00
const vueFlow = useVueFlow("main-flow");
const lf = createFlowFacade(vueFlow);
const loopChildDragContexts = new Map();
window.lf = lf;
const defaultEdgeOptions = {
type: "flow-edge",
animated: true,
selectable: true,
focusable: true,
deletable: true,
interactionWidth: 32,
markerEnd: {
type: MarkerType.ArrowClosed,
color: "#ff6b35",
width: 20,
height: 20,
},
style: { stroke: "#ff6b35", strokeWidth: 2 },
};
2026-07-28 16:26:07 +08:00
const dragover = (e) => {
e.preventDefault();
};
const drop = (e) => {
let node = e.dataTransfer.getData("flowNode");
2026-07-29 11:43:07 +08:00
if (!node) return;
2026-07-28 16:26:07 +08:00
node = JSON.parse(node);
2026-07-29 11:43:07 +08:00
const point = lf.getPointByClient(e.clientX, e.clientY);
const loopNode = findLoopAtPoint(point.canvasOverlayPosition);
if (["stopLoop", "subEnd", "subStart", "currentLoop"].includes(node.type) && !loopNode) {
2026-07-28 16:26:07 +08:00
ElMessage.warning("此节点只能拖拽到循环节点的循环体中");
return;
}
const { type, ...other } = node;
const newNode = lf.addNode({
type,
x: point.canvasOverlayPosition.x,
y: point.canvasOverlayPosition.y,
properties: {
...other,
},
});
2026-07-29 11:43:07 +08:00
if (loopNode && !["start", "end", "loop", "selectArea"].includes(newNode.type)) {
lf.addToGroup(loopNode.id, newNode.id, point.canvasOverlayPosition);
}
2026-07-28 16:26:07 +08:00
if (
!["selectArea", "branch", "stopLoop", "subStart", "subEnd"].includes(newNode.type)
) {
showParamsDrawer.value = true;
paramsDrawerData.value = newNode;
}
};
2026-07-29 11:43:07 +08:00
const handleConnect = (connection) => {
if (!connection.source || !connection.target) return;
const edge = lf.addEdge({
type: "bezier",
sourceNodeId: connection.source,
targetNodeId: connection.target,
sourceAnchorId: connection.sourceHandle || `${connection.source}_1`,
targetAnchorId: connection.targetHandle || `${connection.target}_3`,
});
lf.emit("edge:add", { data: edge });
};
const handleNodeDoubleClick = ({ node }) => {
const data = lf.getGraphData().nodes.find((item) => item.id === node.id);
if (!data || ["selectArea", "branch", "stopLoop", "subStart", "subEnd", "end"].includes(data.type)) return;
showParamsDrawer.value = true;
paramsDrawerData.value = data;
};
const findLoopAtPoint = (point) => {
return (vueFlow.getNodes.value || []).find((candidate) => {
if (candidate.data?.flowType !== "loop") return false;
const groupPosition = candidate.computedPosition || candidate.position;
const groupWidth = Number(candidate.dimensions?.width || candidate.data?.size?.width || 0);
const groupHeight = Number(candidate.dimensions?.height || candidate.data?.size?.height || 0);
return point.x >= groupPosition.x + 24
&& point.x <= groupPosition.x + groupWidth - 24
&& point.y >= groupPosition.y + 112
&& point.y <= groupPosition.y + groupHeight - 24;
});
};
const findContainingLoop = (node) => {
const position = node.computedPosition || node.position;
const width = Number(node.dimensions?.width || node.data?.size?.width || 0);
const height = Number(node.dimensions?.height || node.data?.size?.height || 0);
return findLoopAtPoint({ x: position.x + width / 2, y: position.y + height / 2 });
};
const eventClientPoint = (event) => {
const pointer = event?.touches?.[0] || event?.changedTouches?.[0] || event;
if (!Number.isFinite(pointer?.clientX) || !Number.isFinite(pointer?.clientY)) return null;
return { x: pointer.clientX, y: pointer.clientY };
};
const handleNodeDragStart = ({ node, event }) => {
const drag = lf.prepareLoopChildDrag(node.id);
const clientPoint = eventClientPoint(event);
if (!drag || !clientPoint) return;
const pointer = lf.getPointByClient(clientPoint.x, clientPoint.y).canvasOverlayPosition;
loopChildDragContexts.set(node.id, {
groupId: drag.groupId,
offset: { x: pointer.x - drag.position.x, y: pointer.y - drag.position.y },
});
};
const updateLoopChildDrag = ({ node, event }) => {
const drag = loopChildDragContexts.get(node.id);
const clientPoint = eventClientPoint(event);
if (!drag || !clientPoint) return;
const pointer = lf.getPointByClient(clientPoint.x, clientPoint.y).canvasOverlayPosition;
lf.layoutLoopDuringDrag(drag.groupId, node.id, {
x: pointer.x - drag.offset.x,
y: pointer.y - drag.offset.y,
});
};
const handleNodeDrag = (payload) => updateLoopChildDrag(payload);
const handleNodeDragStop = (payload) => {
const { node } = payload;
updateLoopChildDrag(payload);
loopChildDragContexts.delete(node.id);
const vueNode = vueFlow.findNode(node.id);
if (
vueNode
&& !vueNode.parentNode
&& !["start", "end", "loop", "selectArea"].includes(vueNode.data?.flowType)
) {
const loopNode = findContainingLoop(vueNode);
if (loopNode) lf.addToGroup(loopNode.id, vueNode.id);
}
const graph = lf.getGraphData();
const groups = recursiveFilter(graph.nodes, node.id, ["loop", "selectArea"]);
operationEdge(groups);
lf.markChanged();
};
const handleNodesChange = (changes) => {
if (changes.some((change) => ["add", "remove"].includes(change.type) || (change.type === "position" && change.dragging === false))) {
lf.markChanged();
}
};
const handleEdgesChange = (changes) => {
if (changes.some((change) => ["add", "remove"].includes(change.type))) lf.markChanged();
};
const handleEdgeClick = ({ event, edge }) => {
event?.stopPropagation?.();
lf.selectElementById(edge.id);
};
const handlePaneClick = () => lf.clearSelectElements();
2026-07-28 16:26:07 +08:00
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;
}
if (nodeErrorList.value.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);
}
}
};
2026-07-29 11:43:07 +08:00
let flowInitialized = false;
2026-07-28 16:26:07 +08:00
const initFlow = () => {
2026-07-29 11:43:07 +08:00
if (flowInitialized) return;
flowInitialized = true;
lfRef.value = lf;
loadFlowData(flowInfoData.value.itemId, lf);
lf.on("edge:add", ({ data }) => validEdge(data));
lf.on("history:change", () => {
if (flowStore.disableForm) return;
const flowData = lf.getGraphData();
if (JSON.stringify(oldFlowData) !== JSON.stringify(flowData)) {
2026-07-28 16:26:07 +08:00
flowState.value = "updateData";
2026-07-29 11:43:07 +08:00
}
});
};
2026-07-28 16:26:07 +08:00
2026-07-29 11:43:07 +08:00
const groupSelectionPending = ref(false);
2026-07-28 16:26:07 +08:00
2026-07-29 11:43:07 +08:00
const createGroup = (selectedNodes) => {
const nodes = selectedNodes.filter((item) => !["selectArea", "loop"].includes(item.type));
if (!nodes.length) {
groupSelectionPending.value = false;
return;
2026-07-28 16:26:07 +08:00
}
const PADDING = 200;
2026-07-29 11:43:07 +08:00
let minX = Infinity;
let minY = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
const children = [];
2026-07-28 16:26:07 +08:00
2026-07-29 11:43:07 +08:00
nodes.forEach((item) => {
const model = lf.getNodeModelById(item.id);
const width = model?.width || 372;
const height = model?.height || 180;
minX = Math.min(minX, item.x - width / 2);
minY = Math.min(minY, item.y - height / 2);
maxX = Math.max(maxX, item.x + width / 2);
maxY = Math.max(maxY, item.y + height / 2);
children.push(item.id);
if (model) model.draggable = false;
2026-07-28 16:26:07 +08:00
});
2026-07-29 11:43:07 +08:00
lf.addNode({
type: "selectArea",
x: minX + (maxX - minX) / 2,
y: minY + (maxY - minY) / 2,
properties: {
name: "分组",
action: "NONE",
width: maxX - minX + PADDING,
height: maxY - minY + PADDING,
},
children,
});
lf.clearSelectElements();
groupSelectionPending.value = false;
nextTick(() => lf.fitView());
};
const group = () => {
const { nodes } = lf.getSelectElements();
if (nodes.length) createGroup(nodes);
else groupSelectionPending.value = true;
};
const handleSelectionEnd = () => {
if (!groupSelectionPending.value) return;
createGroup(lf.getSelectElements().nodes);
2026-07-28 16:26:07 +08:00
};
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();
}
});
}
2026-07-29 11:43:07 +08:00
const handleDeleteKey = (event) => {
const activeElement = document.activeElement;
if (["INPUT", "TEXTAREA"].includes(activeElement?.tagName) || activeElement?.isContentEditable) return;
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "z") {
if (flowStore.disableForm) return;
event.preventDefault();
if (event.shiftKey) lf.redo();
else lf.undo();
return;
}
if (!["Backspace", "Delete"].includes(event.key) || flowStore.disableForm) return;
const { nodes, edges } = lf.getSelectElements();
if (nodes.some((node) => ["start", "end"].includes(node.type))) {
lf.clearSelectElements();
return;
}
if (nodes.length) {
event.preventDefault();
nodes.forEach((node) => {
if (node.type === "selectArea") {
const model = lf.getNodeModelById(node.id);
const children = Array.from(model?.children || []);
model.children = [];
children.forEach((childId) => {
const child = lf.getNodeModelById(childId);
if (child) child.draggable = true;
});
}
lf.deleteNode(node.id);
emitter.emit("deleteNode", node.id);
});
return;
}
if (edges.length) {
event.preventDefault();
edges.forEach((edge) => lf.deleteEdge(edge.id));
}
};
2026-07-28 16:26:07 +08:00
const nodeErrorList = ref([]);
onMounted(() => {
justLoadFlow();
initFlow();
document.addEventListener("singleNodeExecution", singleNodeExecution);
registerBeforeUnload();
2026-07-29 11:43:07 +08:00
window.addEventListener("keydown", handleDeleteKey);
2026-07-28 16:26:07 +08:00
emitter.on("openParamsDrawer", (node) => {
showParamsDrawer.value = true;
paramsDrawerData.value = node;
});
emitter.on("throwAnError", (data) => {
// 处理错误抛出逻辑
const { id, hasError, type } = data;
let element
if (type === 'loop') {
const tt = document.getElementsByClassName(`group__container ${id}`)[0];
if (tt?.parentElement) {
element = tt.parentElement;
} else {
element = tt
}
} else {
const tt = document.getElementsByClassName(`node__container ${id}`)[0]
if (tt?.parentElement) {
element = tt.parentElement;
} else {
element = tt
}
}
if (hasError) {
if (!nodeErrorList.value.includes(id)) {
nodeErrorList.value.push(id);
2026-07-29 11:43:07 +08:00
if (element) element.style.background = "#ff00001a";
2026-07-28 16:26:07 +08:00
}
} else {
nodeErrorList.value = nodeErrorList.value.filter((item) => item !== id);
2026-07-29 11:43:07 +08:00
if (element) element.style.background = "#fff";
2026-07-28 16:26:07 +08:00
}
});
emitter.on("deleteNode", (nodeId) => {
const index = nodeErrorList.value.findIndex((item) => item === nodeId);
if (index !== -1) {
nodeErrorList.value.splice(index, 1);
}
})
});
const operationEdge = (arr) => {
arr.forEach((item) => {
newEdge(item.id);
});
};
onUnmounted(() => {
2026-07-29 11:43:07 +08:00
loopChildDragContexts.clear();
2026-07-28 16:26:07 +08:00
emitter.off("changeNodeState");
emitter.off("openParamsDrawer");
emitter.off("throwAnError");
emitter.off("deleteNode");
window.removeEventListener("beforeunload", handleBeforeUnload);
2026-07-29 11:43:07 +08:00
window.removeEventListener("keydown", handleDeleteKey);
2026-07-28 16:26:07 +08:00
document.removeEventListener("singleNodeExecution", singleNodeExecution);
});
</script>
<style lang="scss" scoped>
2026-07-29 11:43:07 +08:00
$flow-canvas-cursor: url("data:image/svg+xml,%3Csvg%20xmlns=%27http://www.w3.org/2000/svg%27%20width=%2724%27%20height=%2724%27%20viewBox=%270%200%2024%2024%27%3E%3Cpath%20d=%27M4%202.5v16l4.2-4%203.1%207.1%203.4-1.5-3.1-7.1h5.8z%27%20fill=%27%23fff%27%20stroke=%27%231f2937%27%20stroke-width=%271.8%27%20stroke-linejoin=%27round%27/%3E%3C/svg%3E") 4 3, default;
2026-07-28 16:26:07 +08:00
.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;
2026-07-29 11:43:07 +08:00
padding: 0 16px;
border-bottom: 1px solid #e3e7ee;
background: #fff;
2026-07-28 16:26:07 +08:00
.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;
2026-07-29 11:43:07 +08:00
border-right: 1px solid #e3e7ee;
background: #f8fafc;
2026-07-28 16:26:07 +08:00
}
.flow__container {
width: 100%;
height: 100%;
2026-07-29 11:43:07 +08:00
cursor: $flow-canvas-cursor;
2026-07-28 16:26:07 +08:00
2026-07-29 11:43:07 +08:00
:deep(.vue-flow__node) {
border: 0;
background: transparent;
cursor: $flow-canvas-cursor;
}
2026-07-28 16:26:07 +08:00
2026-07-29 11:43:07 +08:00
:deep(.vue-flow__node .flow-node-shell),
:deep(.vue-flow__node .flow-node-content),
:deep(.vue-flow__node .node__container),
:deep(.vue-flow__node .right),
:deep(.vue-flow__node .editIcon),
:deep(.vue-flow__node .resultText) {
cursor: $flow-canvas-cursor;
}
2026-07-28 16:26:07 +08:00
2026-07-29 11:43:07 +08:00
:deep(.vue-flow__edge-path),
:deep(.vue-flow__connection-path) {
stroke: #ff6b35;
stroke-width: 2;
}
2026-07-28 16:26:07 +08:00
2026-07-29 11:43:07 +08:00
:deep(.vue-flow__pane) {
cursor: $flow-canvas-cursor;
}
2026-07-28 16:26:07 +08:00
2026-07-29 11:43:07 +08:00
:deep(.vue-flow__pane.dragging) {
cursor: $flow-canvas-cursor;
}
:deep(.vue-flow__handle) {
cursor: crosshair;
}
:deep(.vue-flow__edge) {
cursor: pointer;
}
:deep(.vue-flow__edge-interaction) {
cursor: pointer;
pointer-events: stroke;
}
:deep(.vue-flow__edge.selected .vue-flow__edge-path) {
stroke: #1677ff;
stroke-width: 4;
filter: drop-shadow(0 0 3px rgba(22, 119, 255, 0.45));
}
:deep(.vue-flow__controls) {
overflow: hidden;
border: 1px solid #dfe4ec;
border-radius: 6px;
box-shadow: 0 6px 18px rgba(20, 33, 61, 0.12);
2026-07-28 16:26:07 +08:00
}
}
}
}
</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>