- 将terminalId替换为robotId以统一机器人标识 - 实现机器人设备动态加载和选择功能 - 添加设备类型过滤和状态显示 - 重构设备注册页面为机器人设备管理界面 - 优化API调用参数传递方式 - 添加设备自动匹配和锁定机制
1532 lines
44 KiB
Vue
1532 lines
44 KiB
Vue
<template>
|
|
<div class="page">
|
|
<el-container class="page__container">
|
|
<el-header v-if="!flowInfoData.isLog">
|
|
<div class="flow-heading">
|
|
<div class="flow-heading__name">{{ flowInfoData.name || "未命名流程" }}</div>
|
|
<div class="flow-status" :class="`flow-status--${flowState}`">
|
|
<span class="flow-status__dot"></span>
|
|
<span>{{ stateMap[flowState] }}</span>
|
|
</div>
|
|
</div>
|
|
<div class="btn__container">
|
|
<div class="btn flow-actions">
|
|
<el-tag v-if="hasRunResult && ['testRunError', 'testRunFinish'].includes(flowState)" :type="flowState === 'testRunFinish' ? 'success' : 'danger'">
|
|
{{ totalRunningTime }}ms
|
|
</el-tag>
|
|
<el-button v-if="hasRunResult" @click="clearRun">
|
|
<el-icon><Delete /></el-icon>清除结果
|
|
</el-button>
|
|
<el-button :type="groupSelectionPending ? 'primary' : 'default'" @click="group">
|
|
<el-icon><CollectionTag /></el-icon>{{ groupSelectionPending ? "取消分组" : "分组" }}
|
|
</el-button>
|
|
<el-button v-if="flowState !== 'testRunning'" @click="testRun">
|
|
<el-icon><VideoPlay /></el-icon>试运行
|
|
</el-button>
|
|
<el-button v-if="flowState === 'testRunning'" @click="flowPauseFn">
|
|
<el-icon><VideoPause /></el-icon>暂停
|
|
</el-button>
|
|
<el-button v-if="flowState === 'pause'" @click="flowResumeFn">
|
|
<el-icon><VideoPlay /></el-icon>恢复
|
|
</el-button>
|
|
<el-button v-if="['testRunning', 'pause'].includes(flowState)" @click="flowStopFn">
|
|
<el-icon><CircleClose /></el-icon>终止
|
|
</el-button>
|
|
</div>
|
|
<div class="btn publish-actions">
|
|
<el-tooltip
|
|
class="box-item"
|
|
effect="dark"
|
|
content="试运行成功后才可以发布"
|
|
placement="top"
|
|
v-if="flowState !== 'testRunFinish'"
|
|
>
|
|
<el-button type="primary" :disabled="true"><el-icon><Upload /></el-icon>发布</el-button>
|
|
</el-tooltip>
|
|
<el-button v-else @click="deployFlow" type="primary"><el-icon><Upload /></el-icon>发布</el-button>
|
|
|
|
<el-popover
|
|
:width="260"
|
|
@show="showNodeRelationship"
|
|
popper-class="node-relationship-network-popover"
|
|
>
|
|
<template #reference>
|
|
<el-button circle title="节点关系" aria-label="节点关系"><el-icon><Share /></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="canvas-toolbar">
|
|
<el-tooltip v-if="!flowStore.disableForm" content="撤销" placement="bottom">
|
|
<el-button aria-label="撤销" @click="lf.undo()"><el-icon><RefreshLeft /></el-icon></el-button>
|
|
</el-tooltip>
|
|
<el-tooltip v-if="!flowStore.disableForm" content="重做" placement="bottom">
|
|
<el-button aria-label="重做" @click="lf.redo()"><el-icon><RefreshRight /></el-icon></el-button>
|
|
</el-tooltip>
|
|
<span v-if="!flowStore.disableForm" class="canvas-toolbar__divider"></span>
|
|
<el-tooltip content="适应视图" placement="bottom">
|
|
<el-button aria-label="适应视图" @click="fitView"><el-icon><FullScreen /></el-icon></el-button>
|
|
</el-tooltip>
|
|
</div>
|
|
<VueFlow
|
|
id="main-flow"
|
|
class="flow__container"
|
|
ref="flowContainerRef"
|
|
: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"
|
|
:elevate-nodes-on-select="false"
|
|
:delete-key-code="null"
|
|
:multi-selection-key-code="['Control', 'Meta']"
|
|
:selection-key-code="groupSelectionPending"
|
|
: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"
|
|
@dragover="dragover"
|
|
@drop="drop"
|
|
>
|
|
<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>
|
|
</el-main>
|
|
</el-container>
|
|
</el-container>
|
|
|
|
<TestRun
|
|
:drawer="isOpen"
|
|
@close="isOpen = false"
|
|
@changeState="changeState"
|
|
:flowId="flowInfoData.itemId"
|
|
/>
|
|
|
|
<ParamsDrawer
|
|
v-if="showParamsDrawer"
|
|
:drawer="true"
|
|
:data="paramsDrawerData"
|
|
@close="showParamsDrawer = false"
|
|
/>
|
|
</div>
|
|
|
|
<el-dialog v-model="visible" width="640" class="single-node-dialog" append-to-body>
|
|
<template #header="{ close, titleId, titleClass }">
|
|
<div class="my-header">
|
|
<h4 :id="titleId" :class="titleClass">执行{{ nodeName }}节点</h4>
|
|
</div>
|
|
</template>
|
|
<div v-if="nodeAction === 'AGV_MOVE_TO_STATION'" class="single-node-station-tools">
|
|
<el-button
|
|
type="primary"
|
|
plain
|
|
:loading="stationLoading"
|
|
:disabled="!singleNodeRobotId || !singleNodeDeviceId"
|
|
@click="loadSingleNodeStations"
|
|
>加载站点列表</el-button>
|
|
<span>{{ singleNodeRobotId && singleNodeDeviceId ? '加载后从站点下拉框选择' : '请先选择机器人和设备' }}</span>
|
|
</div>
|
|
<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 class="single-node-param-row">
|
|
<el-form-item
|
|
class="single-node-param-name"
|
|
: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
|
|
class="single-node-param-value"
|
|
: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"
|
|
:max="property.max || Infinity"
|
|
:step="property.step || 1"
|
|
:step-strictly="true"
|
|
placeholder="请输入"
|
|
clearable
|
|
/>
|
|
<el-select
|
|
v-model="property.input"
|
|
v-else-if="property.componentType === 'select'"
|
|
:disabled="property.deviceLocked"
|
|
@change="property.name === 'robotId' && handleSingleNodeRobotChange(property.input)"
|
|
>
|
|
<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"
|
|
resize="none"
|
|
placeholder="请输入"
|
|
clearable
|
|
/>
|
|
<div v-if="property.name === 'deviceId' && property.deviceLocked" class="device-lock-hint">
|
|
已自动匹配当前机器人设备
|
|
</div>
|
|
</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 { 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";
|
|
import { useFlowEvents } from "./composables/useFlowEvents";
|
|
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 { convertToTree } from "@/utils/flow";
|
|
import {
|
|
flowDeploy,
|
|
flowView,
|
|
flowPause,
|
|
flowResume,
|
|
flowStop,
|
|
flowAction,
|
|
getAgvStations,
|
|
} from "@/api/flow/flow";
|
|
import { getDetect } from "@/api/test/detect";
|
|
import { getRobotList, getRobotDevicesByRobotId } from '@/api/inspection/robot'
|
|
import { getDeviceKindForAction, toDeviceOptions } from '@/utils/robotDevice'
|
|
import {
|
|
CircleClose,
|
|
CollectionTag,
|
|
Delete,
|
|
FullScreen,
|
|
Location,
|
|
RefreshLeft,
|
|
RefreshRight,
|
|
Share,
|
|
Upload,
|
|
VideoPause,
|
|
VideoPlay,
|
|
} from "@element-plus/icons-vue";
|
|
|
|
const flowContainerRef = ref(null);
|
|
const lfRef = ref(null);
|
|
const vueFlow = useVueFlow("main-flow");
|
|
const lf = createFlowFacade(vueFlow);
|
|
const loopChildDragContexts = new Map();
|
|
const { onFlowEvent } = useFlowEvents();
|
|
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 },
|
|
};
|
|
|
|
const dragover = (e) => {
|
|
e.preventDefault();
|
|
};
|
|
|
|
const drop = (e) => {
|
|
let node = e.dataTransfer.getData("flowNode");
|
|
if (!node) return;
|
|
node = JSON.parse(node);
|
|
const point = lf.getPointByClient(e.clientX, e.clientY);
|
|
const loopNode = findLoopAtPoint(point.canvasOverlayPosition);
|
|
if (["stopLoop", "subEnd", "subStart", "currentLoop"].includes(node.type) && !loopNode) {
|
|
ElMessage.warning("此节点只能拖拽到循环节点的循环体中");
|
|
return;
|
|
}
|
|
const { type, ...other } = node;
|
|
const newNode = lf.addNode({
|
|
type,
|
|
x: point.canvasOverlayPosition.x,
|
|
y: point.canvasOverlayPosition.y,
|
|
properties: {
|
|
...other,
|
|
},
|
|
});
|
|
if (loopNode && !["start", "end", "loop", "selectArea"].includes(newNode.type)) {
|
|
lf.addToGroup(loopNode.id, newNode.id, point.canvasOverlayPosition);
|
|
}
|
|
|
|
if (
|
|
!["selectArea", "branch", "stopLoop", "subStart", "subEnd"].includes(newNode.type)
|
|
) {
|
|
showParamsDrawer.value = true;
|
|
paramsDrawerData.value = newNode;
|
|
}
|
|
};
|
|
|
|
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);
|
|
}
|
|
|
|
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();
|
|
|
|
const flowStore = useFlowStore();
|
|
|
|
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 hasRunResult = ref(false);
|
|
let flowPollTimer = null;
|
|
let flowDisposed = false;
|
|
|
|
const clearFlowPollTimer = () => {
|
|
if (flowPollTimer !== null) {
|
|
window.clearTimeout(flowPollTimer);
|
|
flowPollTimer = null;
|
|
}
|
|
};
|
|
|
|
const scheduleFlowView = (currentInstId, taskId = null, delay = 2000) => {
|
|
clearFlowPollTimer();
|
|
flowPollTimer = window.setTimeout(() => {
|
|
flowPollTimer = null;
|
|
if (!flowDisposed) loopFlowView(currentInstId, taskId);
|
|
}, delay);
|
|
};
|
|
|
|
const flowResumeFn = async () => {
|
|
const res = await flowResume(instId.value);
|
|
if (res.code === 200) {
|
|
flowState.value = "testRunning";
|
|
clearFlowPollTimer();
|
|
loopFlowView(instId.value);
|
|
ElMessage.success(res.msg);
|
|
}
|
|
};
|
|
|
|
const flowStopFn = async () => {
|
|
const res = await flowStop(instId.value);
|
|
if (res.code === 200) {
|
|
clearFlowPollTimer();
|
|
flowState.value = "stop";
|
|
hasRunResult.value = true;
|
|
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 hasSameEdge = (edge, ignoredId) => lf.getGraphData().edges.some((item) => (
|
|
item.id !== ignoredId
|
|
&& item.sourceNodeId === edge.sourceNodeId
|
|
&& item.targetNodeId === edge.targetNodeId
|
|
&& item.sourceAnchorId === edge.sourceAnchorId
|
|
&& item.targetAnchorId === edge.targetAnchorId
|
|
));
|
|
|
|
const validEdge = (data) => {
|
|
const sourceModel = lf.getNodeModelById(data.sourceNodeId);
|
|
const targetModel = lf.getNodeModelById(data.targetNodeId);
|
|
if (!sourceModel || !targetModel) {
|
|
lf.deleteEdge(data.id);
|
|
return;
|
|
}
|
|
|
|
const sourceAnchorArr = sourceModel.anchors;
|
|
const [sourceName] = sourceAnchorArr
|
|
.filter((item) => {
|
|
return item.id === data.sourceAnchorId;
|
|
})
|
|
.map((item) => {
|
|
return item.name;
|
|
});
|
|
|
|
const targetAnchorArr = targetModel.anchors;
|
|
const [targetName] = targetAnchorArr
|
|
.filter((item) => {
|
|
return item.id === data.targetAnchorId;
|
|
})
|
|
.map((item) => {
|
|
return item.name;
|
|
});
|
|
|
|
if (!sourceName || !targetName) {
|
|
lf.deleteEdge(data.id);
|
|
return;
|
|
}
|
|
|
|
if (sourceName === "right" && targetName === "left") {
|
|
if (hasSameEdge(data, data.id)) lf.deleteEdge(data.id);
|
|
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);
|
|
if (hasSameEdge({ sourceNodeId, targetNodeId, sourceAnchorId, targetAnchorId })) return;
|
|
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;
|
|
hasRunResult.value = false;
|
|
|
|
instId.value = value.instId;
|
|
clearFlowPollTimer();
|
|
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 (flowDisposed) return;
|
|
if (res.code === 200) {
|
|
const arr = res.data || [];
|
|
if (arr.length > 0) hasRunResult.value = true;
|
|
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)
|
|
) {
|
|
scheduleFlowView(instId);
|
|
} 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);
|
|
lf.render(flowData);
|
|
|
|
nextTick(() => {
|
|
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);
|
|
scheduleFlowView(flowInfoData.value.instId, flowInfoData.value.taskId, 1000);
|
|
}
|
|
}
|
|
};
|
|
|
|
let flowInitialized = false;
|
|
const initFlow = () => {
|
|
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)) {
|
|
flowState.value = "updateData";
|
|
}
|
|
});
|
|
};
|
|
|
|
const groupSelectionPending = ref(false);
|
|
|
|
const createGroup = (selectedNodes) => {
|
|
const nodes = selectedNodes.filter((item) => (
|
|
!["selectArea", "loop"].includes(item.type)
|
|
&& !lf.getNodeModelById(item.id)?.parentId
|
|
));
|
|
if (nodes.length < 2) {
|
|
ElMessage.warning("请至少选择两个未分组节点");
|
|
groupSelectionPending.value = false;
|
|
return;
|
|
}
|
|
|
|
const INSET = { left: 52, top: 120, right: 52, bottom: 52 };
|
|
let minX = Infinity;
|
|
let minY = Infinity;
|
|
let maxX = -Infinity;
|
|
let maxY = -Infinity;
|
|
const children = [];
|
|
|
|
nodes.forEach((item) => {
|
|
const model = lf.getNodeModelById(item.id);
|
|
const width = model?.width || 372;
|
|
const height = model?.height || 180;
|
|
const left = Number.isFinite(model?.left) ? model.left : item.x - width / 2;
|
|
const top = Number.isFinite(model?.top) ? model.top : item.y - height / 2;
|
|
minX = Math.min(minX, left);
|
|
minY = Math.min(minY, top);
|
|
maxX = Math.max(maxX, left + width);
|
|
maxY = Math.max(maxY, top + height);
|
|
children.push(item.id);
|
|
});
|
|
|
|
const width = maxX - minX + INSET.left + INSET.right;
|
|
const height = maxY - minY + INSET.top + INSET.bottom;
|
|
lf.groupNodes({
|
|
type: "selectArea",
|
|
x: minX - INSET.left + width / 2,
|
|
y: minY - INSET.top + height / 2,
|
|
properties: {
|
|
name: "分组",
|
|
action: "NONE",
|
|
width,
|
|
height,
|
|
},
|
|
}, children);
|
|
lf.clearSelectElements();
|
|
groupSelectionPending.value = false;
|
|
};
|
|
|
|
const group = () => {
|
|
if (groupSelectionPending.value) {
|
|
groupSelectionPending.value = false;
|
|
lf.clearSelectElements();
|
|
return;
|
|
}
|
|
const { nodes } = lf.getSelectElements();
|
|
if (nodes.length) createGroup(nodes);
|
|
else groupSelectionPending.value = true;
|
|
};
|
|
|
|
const handleSelectionEnd = () => {
|
|
if (!groupSelectionPending.value) return;
|
|
createGroup(lf.getSelectElements().nodes);
|
|
};
|
|
|
|
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 stationLoading = ref(false);
|
|
const singleNodeRobotOptions = ref([]);
|
|
const singleNodeRobotId = computed(() =>
|
|
formData.nodeParams.find((item) => item.name === "robotId")?.input
|
|
);
|
|
const singleNodeDeviceId = computed(() =>
|
|
formData.nodeParams.find((item) => item.name === "deviceId")?.input
|
|
);
|
|
|
|
const handleSingleNodeRobotChange = async (robotId) => {
|
|
const deviceParam = formData.nodeParams.find((item) => item.name === "deviceId");
|
|
if (!deviceParam) return;
|
|
deviceParam.input = "";
|
|
deviceParam.deviceLocked = false;
|
|
deviceParam.componentType = "select";
|
|
deviceParam.selectOptions = [];
|
|
const stationParam = formData.nodeParams.find((item) => item.name === "stationId");
|
|
if (stationParam) {
|
|
stationParam.input = "";
|
|
stationParam.selectOptions = [];
|
|
}
|
|
if (!robotId) return;
|
|
try {
|
|
const deviceKind = getDeviceKindForAction(nodeAction.value);
|
|
const response = await getRobotDevicesByRobotId(robotId,
|
|
deviceKind ? { deviceKind } : {});
|
|
const devices = Array.isArray(response.data) ? response.data : [];
|
|
deviceParam.selectOptions = toDeviceOptions(devices);
|
|
if (devices.length > 0) {
|
|
deviceParam.input = devices[0].deviceId;
|
|
deviceParam.deviceLocked = devices.length === 1;
|
|
} else {
|
|
ElMessage.warning("当前机器人没有匹配的在线设备");
|
|
}
|
|
} catch (error) {
|
|
console.error("加载机器人设备失败:", error);
|
|
ElMessage.error("加载机器人设备失败");
|
|
}
|
|
};
|
|
|
|
const stationOptions = (stations) => (Array.isArray(stations) ? stations : [])
|
|
.filter((station) => station?.id !== undefined && station?.id !== null && String(station.id).trim())
|
|
.map((station) => {
|
|
const id = String(station.id).trim();
|
|
const description = station.description ? String(station.description).trim() : "";
|
|
return { value: id, label: description ? `${id} - ${description}` : id };
|
|
});
|
|
|
|
const loadSingleNodeStations = async () => {
|
|
stationLoading.value = true;
|
|
try {
|
|
const response = await getAgvStations({
|
|
robotId: singleNodeRobotId.value,
|
|
deviceId: singleNodeDeviceId.value,
|
|
});
|
|
const options = stationOptions(response?.data);
|
|
const stationParam = formData.nodeParams.find((item) => item.name === "stationId");
|
|
if (!stationParam) return;
|
|
stationParam.selectOptions = options;
|
|
stationParam.stationRobotId = singleNodeRobotId.value;
|
|
stationParam.stationDeviceId = singleNodeDeviceId.value;
|
|
if (!options.some((item) => item.value === stationParam.input)) stationParam.input = "";
|
|
if (options.length === 0) ElMessage.warning("当前AGV没有可用站点");
|
|
} catch (error) {
|
|
console.error("加载AGV站点列表失败:", error);
|
|
ElMessage.error("加载AGV站点列表失败");
|
|
} finally {
|
|
stationLoading.value = false;
|
|
}
|
|
};
|
|
|
|
const loadSingleNodeRobots = async () => {
|
|
const response = await getRobotList({ pageNum: 1, pageSize: 1000, connectStatus: '1' });
|
|
singleNodeRobotOptions.value = (response.rows || []).map(item => ({
|
|
label: `${item.robotName || item.robotId} (${item.robotId})`,
|
|
value: item.robotId,
|
|
}));
|
|
};
|
|
|
|
const singleNodeExecution = async (e) => {
|
|
const { name, nodeType, nodeParams, action } = e.detail;
|
|
const executionParams = JSON.parse(JSON.stringify(nodeParams || []));
|
|
nodeName.value = name;
|
|
visible.value = true;
|
|
nodeAction.value = action;
|
|
stationLoading.value = false;
|
|
if (nodeType === "EDGE") {
|
|
await loadSingleNodeRobots();
|
|
formData.nodeParams = [
|
|
{
|
|
disabled: true,
|
|
input: "",
|
|
name: "robotId",
|
|
type: "input",
|
|
componentType: "select",
|
|
selectOptions: singleNodeRobotOptions.value,
|
|
},
|
|
...executionParams,
|
|
];
|
|
} else {
|
|
formData.nodeParams = executionParams;
|
|
}
|
|
if (action === "AGV_MOVE_TO_STATION") {
|
|
const stationParam = formData.nodeParams.find((item) => item.name === "stationId");
|
|
if (stationParam) {
|
|
stationParam.input = "";
|
|
stationParam.selectOptions = [];
|
|
}
|
|
}
|
|
};
|
|
|
|
const execute = async () => {
|
|
if (nodeAction.value === "AGV_MOVE_TO_STATION") {
|
|
const stationParam = formData.nodeParams.find((item) => item.name === "stationId");
|
|
const validStation = stationParam?.selectOptions?.some((item) => item.value === stationParam.input);
|
|
const correctSource = stationParam?.stationRobotId === singleNodeRobotId.value
|
|
&& stationParam?.stationDeviceId === singleNodeDeviceId.value;
|
|
if (!validStation || !correctSource) {
|
|
ElMessage.warning("请按当前机器人和设备重新加载并选择站点");
|
|
return;
|
|
}
|
|
}
|
|
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();
|
|
}
|
|
});
|
|
hasRunResult.value = false;
|
|
totalRunningTime.value = 0;
|
|
}
|
|
|
|
const NODE_PASTE_OFFSET = 40;
|
|
const NON_COPYABLE_NODE_TYPES = new Set(["start", "end", "selectArea"]);
|
|
let copiedNodes = [];
|
|
let pasteCount = 0;
|
|
|
|
const isTextEditing = (event) => {
|
|
const element = event.target || document.activeElement;
|
|
return ["INPUT", "TEXTAREA", "SELECT"].includes(element?.tagName)
|
|
|| element?.isContentEditable;
|
|
};
|
|
|
|
const copySelectedNodes = (event) => {
|
|
const nodes = lf.getSelectElements().nodes.filter(
|
|
(node) => !NON_COPYABLE_NODE_TYPES.has(node.type),
|
|
);
|
|
if (!nodes.length) return;
|
|
|
|
event.preventDefault();
|
|
copiedNodes = JSON.parse(JSON.stringify(nodes));
|
|
pasteCount = 0;
|
|
};
|
|
|
|
const pasteCopiedNodes = (event) => {
|
|
if (!copiedNodes.length || flowStore.disableForm) return;
|
|
|
|
event.preventDefault();
|
|
pasteCount += 1;
|
|
const offset = NODE_PASTE_OFFSET * pasteCount;
|
|
const pastedNodes = copiedNodes.map((sourceNode) => {
|
|
const properties = JSON.parse(JSON.stringify(sourceNode.properties || {}));
|
|
const parentId = properties.parentId;
|
|
delete properties.parentId;
|
|
|
|
const newNode = lf.addNode({
|
|
type: sourceNode.type,
|
|
x: Number(sourceNode.x || 0) + offset,
|
|
y: Number(sourceNode.y || 0) + offset,
|
|
properties,
|
|
});
|
|
|
|
if (sourceNode.type === "branch" && Array.isArray(properties.anchor)) {
|
|
const oldElseId = `${sourceNode.id}_else`;
|
|
const newElseId = `${newNode.id}_else`;
|
|
const anchors = properties.anchor.map((anchor) => (
|
|
anchor.id === oldElseId ? { ...anchor, id: newElseId } : anchor
|
|
));
|
|
lf.setProperties(newNode.id, { ...properties, anchor: anchors });
|
|
}
|
|
|
|
if (parentId && lf.getNodeModelById(parentId)) {
|
|
lf.addToGroup(parentId, newNode.id, { x: newNode.x, y: newNode.y });
|
|
}
|
|
return newNode;
|
|
});
|
|
|
|
lf.clearSelectElements();
|
|
if (pastedNodes.length === 1) lf.selectElementById(pastedNodes[0].id);
|
|
};
|
|
|
|
const handleKeyboardShortcut = (event) => {
|
|
if (isTextEditing(event)) return;
|
|
|
|
const isModifierKey = event.ctrlKey || event.metaKey;
|
|
const key = event.key.toLowerCase();
|
|
if (isModifierKey && key === "c") {
|
|
if (!flowStore.disableForm) copySelectedNodes(event);
|
|
return;
|
|
}
|
|
if (isModifierKey && key === "v") {
|
|
pasteCopiedNodes(event);
|
|
return;
|
|
}
|
|
|
|
if (isModifierKey && key === "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) => {
|
|
lf.deleteNode(node.id);
|
|
emitter.emit("deleteNode", node.id);
|
|
});
|
|
return;
|
|
}
|
|
if (edges.length) {
|
|
event.preventDefault();
|
|
edges.forEach((edge) => lf.deleteEdge(edge.id));
|
|
}
|
|
};
|
|
|
|
const nodeErrorList = ref([]);
|
|
|
|
onMounted(() => {
|
|
justLoadFlow();
|
|
initFlow();
|
|
document.addEventListener("singleNodeExecution", singleNodeExecution);
|
|
registerBeforeUnload();
|
|
window.addEventListener("keydown", handleKeyboardShortcut);
|
|
|
|
onFlowEvent("openParamsDrawer", (node) => {
|
|
showParamsDrawer.value = true;
|
|
paramsDrawerData.value = node;
|
|
});
|
|
|
|
onFlowEvent("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);
|
|
if (element) element.style.background = "#ff00001a";
|
|
}
|
|
} else {
|
|
nodeErrorList.value = nodeErrorList.value.filter((item) => item !== id);
|
|
if (element) element.style.background = "#fff";
|
|
}
|
|
});
|
|
|
|
onFlowEvent("deleteNode", (nodeId) => {
|
|
const index = nodeErrorList.value.findIndex((item) => item === nodeId);
|
|
if (index !== -1) {
|
|
nodeErrorList.value.splice(index, 1);
|
|
}
|
|
})
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
flowDisposed = true;
|
|
clearFlowPollTimer();
|
|
loopChildDragContexts.clear();
|
|
if (window.lf === lf) delete window.lf;
|
|
|
|
window.removeEventListener("beforeunload", handleBeforeUnload);
|
|
window.removeEventListener("keydown", handleKeyboardShortcut);
|
|
document.removeEventListener("singleNodeExecution", singleNodeExecution);
|
|
});
|
|
</script>
|
|
<style lang="scss" scoped>
|
|
$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;
|
|
|
|
.single-node-param-row {
|
|
display: grid;
|
|
grid-template-columns: 190px minmax(0, 1fr);
|
|
gap: 16px;
|
|
align-items: start;
|
|
}
|
|
|
|
.single-node-param-name,
|
|
.single-node-param-value {
|
|
width: 100%;
|
|
margin-right: 0;
|
|
margin-bottom: 16px;
|
|
|
|
:deep(.el-form-item__content),
|
|
:deep(.el-input),
|
|
:deep(.el-input-number),
|
|
:deep(.el-select) {
|
|
width: 100%;
|
|
}
|
|
}
|
|
|
|
.single-node-param-value :deep(.el-textarea__inner) {
|
|
min-height: 32px !important;
|
|
line-height: 20px;
|
|
}
|
|
|
|
.single-node-station-tools {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
margin-bottom: 16px;
|
|
padding: 10px 12px;
|
|
border: 1px solid #dfe6ef;
|
|
border-radius: 6px;
|
|
background: #f7f9fc;
|
|
color: #65758b;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.device-lock-hint {
|
|
width: 100%;
|
|
margin-top: 6px;
|
|
color: var(--el-color-success);
|
|
font-size: 12px;
|
|
line-height: 18px;
|
|
}
|
|
|
|
.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;
|
|
padding: 0 16px;
|
|
border-bottom: 1px solid #e3e7ee;
|
|
background: #fff;
|
|
gap: 18px;
|
|
|
|
.flow-heading {
|
|
display: flex;
|
|
min-width: 0;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
.flow-heading__name {
|
|
overflow: hidden;
|
|
max-width: 320px;
|
|
color: #1f2937;
|
|
font-size: 16px;
|
|
font-weight: 650;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.flow-status {
|
|
display: flex;
|
|
height: 24px;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 0 8px;
|
|
border: 1px solid #dfe4ec;
|
|
border-radius: 5px;
|
|
background: #f8fafc;
|
|
color: #687386;
|
|
font-size: 11px;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.flow-status__dot {
|
|
width: 6px;
|
|
height: 6px;
|
|
border-radius: 50%;
|
|
background: currentColor;
|
|
}
|
|
|
|
.flow-status--release,
|
|
.flow-status--testRunFinish {
|
|
border-color: #c8ead5;
|
|
background: #effaf3;
|
|
color: #1e7d49;
|
|
}
|
|
|
|
.flow-status--testRunning,
|
|
.flow-status--updateData {
|
|
border-color: #cbdcf8;
|
|
background: #f0f6ff;
|
|
color: #2f6fca;
|
|
}
|
|
|
|
.flow-status--testRunError,
|
|
.flow-status--stop {
|
|
border-color: #f1cecb;
|
|
background: #fff3f2;
|
|
color: #c33b32;
|
|
}
|
|
|
|
.flow-status--pause {
|
|
border-color: #ead9b8;
|
|
background: #fff9eb;
|
|
color: #9a6700;
|
|
}
|
|
|
|
.btn__container {
|
|
display: flex;
|
|
min-width: 0;
|
|
align-items: center;
|
|
gap: 10px;
|
|
|
|
.btn {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
|
|
.el-button + .el-button { margin-left: 0; }
|
|
}
|
|
|
|
.publish-actions {
|
|
padding-left: 10px;
|
|
border-left: 1px solid #e4e8ef;
|
|
}
|
|
}
|
|
}
|
|
|
|
.container {
|
|
height: calc(100% - 60px);
|
|
}
|
|
|
|
.el-main {
|
|
padding: 0;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.el-aside {
|
|
margin: 0;
|
|
width: 272px;
|
|
padding: 0;
|
|
border-right: 1px solid #e3e7ee;
|
|
background: #f8fafc;
|
|
}
|
|
|
|
.canvas-toolbar {
|
|
display: flex;
|
|
position: absolute;
|
|
top: 14px;
|
|
left: 14px;
|
|
z-index: 20;
|
|
align-items: center;
|
|
padding: 4px;
|
|
border: 1px solid #dfe4ec;
|
|
border-radius: 7px;
|
|
background: rgba(255, 255, 255, 0.96);
|
|
box-shadow: 0 6px 18px rgba(31, 41, 55, 0.1);
|
|
|
|
.el-button {
|
|
width: 30px;
|
|
height: 30px;
|
|
padding: 0;
|
|
border: 0;
|
|
border-radius: 5px;
|
|
background: transparent;
|
|
color: #566174;
|
|
}
|
|
|
|
.el-button + .el-button { margin-left: 2px; }
|
|
|
|
.el-button:hover {
|
|
background: #f1f5f9;
|
|
color: #1f2937;
|
|
}
|
|
}
|
|
|
|
.canvas-toolbar__divider {
|
|
width: 1px;
|
|
height: 18px;
|
|
margin: 0 4px;
|
|
background: #e1e6ed;
|
|
}
|
|
|
|
.flow__container {
|
|
width: 100%;
|
|
height: 100%;
|
|
cursor: $flow-canvas-cursor;
|
|
|
|
:deep(.vue-flow__node) {
|
|
border: 0;
|
|
background: transparent;
|
|
cursor: $flow-canvas-cursor;
|
|
}
|
|
|
|
: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;
|
|
}
|
|
|
|
:deep(.vue-flow__edge-path),
|
|
:deep(.vue-flow__connection-path) {
|
|
stroke: #ff6b35;
|
|
stroke-width: 2;
|
|
}
|
|
|
|
:deep(.vue-flow__pane) {
|
|
cursor: $flow-canvas-cursor;
|
|
}
|
|
|
|
: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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</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>
|