2026-08-07 14:38:16 +08:00
|
|
|
|
<!--
|
|
|
|
|
|
* 文件说明:流程设计器主页面,负责编辑、校验、试运行、执行跟踪和快捷操作。
|
|
|
|
|
|
* 作用范围:仅服务于流程设计器模块。
|
|
|
|
|
|
-->
|
2026-07-28 16:26:07 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="page">
|
|
|
|
|
|
<el-container class="page__container">
|
|
|
|
|
|
<el-header v-if="!flowInfoData.isLog">
|
2026-07-29 16:17:39 +08:00
|
|
|
|
<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>
|
2026-07-28 16:26:07 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="btn__container">
|
2026-07-29 16:17:39 +08:00
|
|
|
|
<div class="btn flow-actions">
|
|
|
|
|
|
<el-tag v-if="hasRunResult && ['testRunError', 'testRunFinish'].includes(flowState)" :type="flowState === 'testRunFinish' ? 'success' : 'danger'">
|
2026-07-28 16:26:07 +08:00
|
|
|
|
{{ totalRunningTime }}ms
|
|
|
|
|
|
</el-tag>
|
2026-07-29 16:17:39 +08:00
|
|
|
|
<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>
|
2026-07-28 16:26:07 +08:00
|
|
|
|
</div>
|
2026-07-29 16:17:39 +08:00
|
|
|
|
<div class="btn publish-actions">
|
2026-07-28 16:26:07 +08:00
|
|
|
|
<el-tooltip
|
|
|
|
|
|
class="box-item"
|
|
|
|
|
|
effect="dark"
|
|
|
|
|
|
content="试运行成功后才可以发布"
|
|
|
|
|
|
placement="top"
|
|
|
|
|
|
v-if="flowState !== 'testRunFinish'"
|
|
|
|
|
|
>
|
2026-07-29 16:17:39 +08:00
|
|
|
|
<el-button type="primary" :disabled="true"><el-icon><Upload /></el-icon>发布</el-button>
|
2026-07-28 16:26:07 +08:00
|
|
|
|
</el-tooltip>
|
2026-07-29 16:17:39 +08:00
|
|
|
|
<el-button v-else @click="deployFlow" type="primary"><el-icon><Upload /></el-icon>发布</el-button>
|
2026-07-28 16:26:07 +08:00
|
|
|
|
|
|
|
|
|
|
<el-popover
|
|
|
|
|
|
:width="260"
|
|
|
|
|
|
@show="showNodeRelationship"
|
|
|
|
|
|
popper-class="node-relationship-network-popover"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #reference>
|
2026-07-29 16:17:39 +08:00
|
|
|
|
<el-button circle title="节点关系" aria-label="节点关系"><el-icon><Share /></el-icon></el-button>
|
2026-07-28 16:26:07 +08:00
|
|
|
|
</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 16:17:39 +08:00
|
|
|
|
<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>
|
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"
|
2026-07-29 16:17:39 +08:00
|
|
|
|
:elevate-nodes-on-select="false"
|
2026-07-29 11:43:07 +08:00
|
|
|
|
:delete-key-code="null"
|
|
|
|
|
|
:multi-selection-key-code="['Control', 'Meta']"
|
2026-08-05 15:18:52 +08:00
|
|
|
|
:selection-key-code="flowStore.disableForm ? null : true"
|
|
|
|
|
|
:pan-on-drag="flowStore.disableForm ? true : [1, 2]"
|
2026-07-29 11:43:07 +08:00
|
|
|
|
: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
|
2026-07-29 16:17:39 +08:00
|
|
|
|
v-if="showParamsDrawer"
|
|
|
|
|
|
:drawer="true"
|
2026-07-28 16:26:07 +08:00
|
|
|
|
:data="paramsDrawerData"
|
|
|
|
|
|
@close="showParamsDrawer = false"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-08-05 09:53:23 +08:00
|
|
|
|
<el-dialog v-model="visible" width="500" class="single-node-dialog" append-to-body>
|
2026-07-28 16:26:07 +08:00
|
|
|
|
<template #header="{ close, titleId, titleClass }">
|
|
|
|
|
|
<div class="my-header">
|
|
|
|
|
|
<h4 :id="titleId" :class="titleClass">执行{{ nodeName }}节点</h4>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
2026-08-04 11:46:14 +08:00
|
|
|
|
<div v-if="nodeAction === 'AGV_MOVE_TO_STATION'" class="single-node-station-tools">
|
|
|
|
|
|
<el-button
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
plain
|
|
|
|
|
|
:loading="stationLoading"
|
2026-08-04 17:29:07 +08:00
|
|
|
|
:disabled="!singleNodeRobotId || !singleNodeDeviceId"
|
2026-08-04 11:46:14 +08:00
|
|
|
|
@click="loadSingleNodeStations"
|
|
|
|
|
|
>加载站点列表</el-button>
|
2026-08-04 17:29:07 +08:00
|
|
|
|
<span>{{ singleNodeRobotId && singleNodeDeviceId ? '加载后从站点下拉框选择' : '请先选择机器人和设备' }}</span>
|
2026-08-04 11:46:14 +08:00
|
|
|
|
</div>
|
2026-07-28 16:26:07 +08:00
|
|
|
|
<el-form
|
|
|
|
|
|
:inline="true"
|
|
|
|
|
|
:model="formData"
|
|
|
|
|
|
:rules="rules"
|
|
|
|
|
|
ref="executeForm"
|
|
|
|
|
|
label-position="top"
|
|
|
|
|
|
label-width="auto"
|
|
|
|
|
|
:disabled="flowStore.disableForm"
|
|
|
|
|
|
>
|
2026-08-07 10:48:18 +08:00
|
|
|
|
<div v-for="(field, index) in singleNodeFields" :key="field.key">
|
2026-08-05 09:27:55 +08:00
|
|
|
|
<el-row class="single-node-param-row">
|
2026-07-28 16:26:07 +08:00
|
|
|
|
<el-form-item
|
2026-08-05 09:27:55 +08:00
|
|
|
|
class="single-node-param-name"
|
2026-07-28 16:26:07 +08:00
|
|
|
|
:label="index === 0 ? '参数名' : ''"
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-input
|
2026-08-07 10:48:18 +08:00
|
|
|
|
:disabled="field.param?.disabled || false"
|
|
|
|
|
|
v-model="field.param.name"
|
2026-07-28 16:26:07 +08:00
|
|
|
|
placeholder="请输入"
|
|
|
|
|
|
clearable
|
|
|
|
|
|
/>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item
|
2026-08-05 09:27:55 +08:00
|
|
|
|
class="single-node-param-value"
|
2026-07-28 16:26:07 +08:00
|
|
|
|
:label="index === 0 ? '参数值' : ''"
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-input-number
|
2026-08-07 10:48:18 +08:00
|
|
|
|
v-if="field.param.componentType === 'number'"
|
|
|
|
|
|
v-model="field.param.input"
|
2026-07-28 16:26:07 +08:00
|
|
|
|
:min="0"
|
2026-08-07 10:48:18 +08:00
|
|
|
|
:max="field.param.max || Infinity"
|
|
|
|
|
|
:step="field.param.step || 1"
|
2026-07-28 16:26:07 +08:00
|
|
|
|
:step-strictly="true"
|
|
|
|
|
|
placeholder="请输入"
|
|
|
|
|
|
clearable
|
|
|
|
|
|
/>
|
|
|
|
|
|
<el-select
|
2026-08-07 10:48:18 +08:00
|
|
|
|
v-model="field.param.input"
|
|
|
|
|
|
v-else-if="field.param.componentType === 'select'"
|
|
|
|
|
|
:disabled="field.param.deviceLocked"
|
|
|
|
|
|
@change="field.param.name === 'robotId' && handleSingleNodeRobotChange(field.param.input)"
|
2026-07-28 16:26:07 +08:00
|
|
|
|
>
|
|
|
|
|
|
<el-option
|
2026-08-07 10:48:18 +08:00
|
|
|
|
v-for="item in field.param.selectOptions"
|
2026-07-28 16:26:07 +08:00
|
|
|
|
:key="item.value"
|
|
|
|
|
|
:label="item.label"
|
|
|
|
|
|
:value="item.value"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
<el-input
|
|
|
|
|
|
v-else
|
2026-08-07 10:48:18 +08:00
|
|
|
|
v-model="field.param.input"
|
2026-07-28 16:26:07 +08:00
|
|
|
|
autosize
|
|
|
|
|
|
type="textarea"
|
2026-08-05 09:27:55 +08:00
|
|
|
|
resize="none"
|
2026-07-28 16:26:07 +08:00
|
|
|
|
placeholder="请输入"
|
|
|
|
|
|
clearable
|
|
|
|
|
|
/>
|
2026-08-07 10:48:18 +08:00
|
|
|
|
<div v-if="field.param.name === 'deviceId' && field.param.deviceLocked" class="device-lock-hint">
|
2026-08-05 09:27:55 +08:00
|
|
|
|
已自动匹配当前机器人设备
|
|
|
|
|
|
</div>
|
2026-07-28 16:26:07 +08:00
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-row>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-form>
|
|
|
|
|
|
<template #footer>
|
|
|
|
|
|
<div class="dialog-footer">
|
|
|
|
|
|
<el-button @click="visible = false">取消</el-button>
|
2026-08-07 10:48:18 +08:00
|
|
|
|
<el-button type="primary" :loading="singleNodeExecuting" @click="execute"> 确定 </el-button>
|
2026-07-28 16:26:07 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-dialog>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
2026-08-07 11:28:01 +08:00
|
|
|
|
import { computed, reactive, ref, watch, onMounted, onUnmounted, nextTick } from "vue";
|
2026-07-29 11:43:07 +08:00
|
|
|
|
import { MarkerType, VueFlow, useVueFlow } from "@vue-flow/core";
|
2026-08-05 15:18:52 +08:00
|
|
|
|
import { v4 as uuid } from "uuid";
|
2026-07-29 11:43:07 +08:00
|
|
|
|
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-29 16:17:39 +08:00
|
|
|
|
import { useFlowEvents } from "./composables/useFlowEvents";
|
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";
|
2026-07-29 16:17:39 +08:00
|
|
|
|
import { convertToTree } from "@/utils/flow";
|
2026-08-11 16:50:30 +08:00
|
|
|
|
import {
|
|
|
|
|
|
actionRequiresDevice,
|
|
|
|
|
|
createDefaultRobotRole,
|
|
|
|
|
|
ensureNodeExecutionResources,
|
|
|
|
|
|
getResourceRoles,
|
|
|
|
|
|
mergeCopiedResourceRoles,
|
|
|
|
|
|
normalizeFlowResourceSchema,
|
|
|
|
|
|
validateFlowResources,
|
|
|
|
|
|
} from "@/utils/flowResources";
|
2026-07-28 16:26:07 +08:00
|
|
|
|
import {
|
|
|
|
|
|
flowDeploy,
|
|
|
|
|
|
flowView,
|
|
|
|
|
|
flowPause,
|
|
|
|
|
|
flowResume,
|
|
|
|
|
|
flowStop,
|
|
|
|
|
|
flowAction,
|
2026-08-04 11:46:14 +08:00
|
|
|
|
getAgvStations,
|
2026-08-04 16:30:00 +08:00
|
|
|
|
} from "@/api/flow/flow";
|
2026-07-28 16:26:07 +08:00
|
|
|
|
import { getDetect } from "@/api/test/detect";
|
2026-08-05 09:27:55 +08:00
|
|
|
|
import { getRobotList, getRobotDevicesByRobotId } from '@/api/inspection/robot'
|
|
|
|
|
|
import { getDeviceKindForAction, toDeviceOptions } from '@/utils/robotDevice'
|
2026-07-29 16:17:39 +08:00
|
|
|
|
import {
|
|
|
|
|
|
CircleClose,
|
|
|
|
|
|
CollectionTag,
|
|
|
|
|
|
Delete,
|
|
|
|
|
|
FullScreen,
|
|
|
|
|
|
Location,
|
|
|
|
|
|
RefreshLeft,
|
|
|
|
|
|
RefreshRight,
|
|
|
|
|
|
Share,
|
|
|
|
|
|
Upload,
|
|
|
|
|
|
VideoPause,
|
|
|
|
|
|
VideoPlay,
|
|
|
|
|
|
} from "@element-plus/icons-vue";
|
2026-07-28 16:26:07 +08:00
|
|
|
|
|
|
|
|
|
|
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();
|
2026-07-29 16:17:39 +08:00
|
|
|
|
const { onFlowEvent } = useFlowEvents();
|
2026-07-29 11:43:07 +08:00
|
|
|
|
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
|
|
|
|
|
2026-08-07 14:38:16 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 处理 dragover 对应的数据或交互;作用范围仅限当前组件或模块。
|
|
|
|
|
|
* @param {*} e 触发操作的浏览器事件
|
|
|
|
|
|
*/
|
2026-07-28 16:26:07 +08:00
|
|
|
|
const dragover = (e) => {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-08-07 14:38:16 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 处理 drop 对应的数据或交互;作用范围仅限当前组件或模块。
|
|
|
|
|
|
* @param {*} e 触发操作的浏览器事件
|
|
|
|
|
|
*/
|
2026-07-28 16:26:07 +08:00
|
|
|
|
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-08-11 16:50:30 +08:00
|
|
|
|
const normalizedResources = ensureNodeExecutionResources(lf.getGraphData(), newNode.id);
|
|
|
|
|
|
if (normalizedResources.start) {
|
|
|
|
|
|
lf.setProperties(normalizedResources.start.id, normalizedResources.start.properties);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (normalizedResources.node) {
|
|
|
|
|
|
lf.setProperties(normalizedResources.node.id, normalizedResources.node.properties);
|
|
|
|
|
|
}
|
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 (
|
2026-08-07 15:27:59 +08:00
|
|
|
|
!["selectArea", "stopLoop", "subStart", "subEnd"].includes(newNode.type)
|
2026-07-28 16:26:07 +08:00
|
|
|
|
) {
|
|
|
|
|
|
showParamsDrawer.value = true;
|
2026-08-11 16:50:30 +08:00
|
|
|
|
paramsDrawerData.value = lf.getGraphData().nodes.find((item) => item.id === newNode.id) || newNode;
|
2026-07-28 16:26:07 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-08-07 14:38:16 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 处理 handleConnect 对应的数据或交互;作用范围仅限当前组件或模块。
|
|
|
|
|
|
* @param {*} connection 连接信息
|
|
|
|
|
|
*/
|
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 });
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-08-07 14:38:16 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 处理 handleNodeDoubleClick 对应的数据或交互;作用范围仅限当前组件或模块。
|
|
|
|
|
|
* @param {*} { node } 调用方传入的 { node } 参数
|
|
|
|
|
|
*/
|
2026-07-29 11:43:07 +08:00
|
|
|
|
const handleNodeDoubleClick = ({ node }) => {
|
|
|
|
|
|
const data = lf.getGraphData().nodes.find((item) => item.id === node.id);
|
2026-08-07 15:27:59 +08:00
|
|
|
|
if (!data || ["selectArea", "stopLoop", "subStart", "subEnd", "end"].includes(data.type)) return;
|
2026-07-29 11:43:07 +08:00
|
|
|
|
showParamsDrawer.value = true;
|
|
|
|
|
|
paramsDrawerData.value = data;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-08-07 14:38:16 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 查找 findLoopAtPoint 对应的数据或交互;作用范围仅限当前组件或模块。
|
|
|
|
|
|
* @param {*} point 画布坐标
|
|
|
|
|
|
*/
|
2026-07-29 11:43:07 +08:00
|
|
|
|
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;
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-08-07 14:38:16 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 查找 findContainingLoop 对应的数据或交互;作用范围仅限当前组件或模块。
|
|
|
|
|
|
* @param {*} node 当前流程节点
|
|
|
|
|
|
*/
|
2026-07-29 11:43:07 +08:00
|
|
|
|
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 });
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-08-07 14:38:16 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 处理 eventClientPoint 对应的数据或交互;作用范围仅限当前组件或模块。
|
|
|
|
|
|
* @param {*} event 触发操作的浏览器事件
|
|
|
|
|
|
*/
|
2026-07-29 11:43:07 +08:00
|
|
|
|
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 };
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-08-07 14:38:16 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 处理 handleNodeDragStart 对应的数据或交互;作用范围仅限当前组件或模块。
|
|
|
|
|
|
* @param {*} { node 调用方传入的 { node 参数
|
|
|
|
|
|
* @param {*} event } 调用方传入的 event } 参数
|
|
|
|
|
|
*/
|
2026-07-29 11:43:07 +08:00
|
|
|
|
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 },
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-08-07 14:38:16 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 更新 updateLoopChildDrag 对应的数据或交互;作用范围仅限当前组件或模块。
|
|
|
|
|
|
* @param {*} { node 调用方传入的 { node 参数
|
|
|
|
|
|
* @param {*} event } 调用方传入的 event } 参数
|
|
|
|
|
|
*/
|
2026-07-29 11:43:07 +08:00
|
|
|
|
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,
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-08-07 14:38:16 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 处理 handleNodeDrag 对应的数据或交互;作用范围仅限当前组件或模块。
|
|
|
|
|
|
* @param {*} payload 事件数据
|
|
|
|
|
|
*/
|
2026-07-29 11:43:07 +08:00
|
|
|
|
const handleNodeDrag = (payload) => updateLoopChildDrag(payload);
|
|
|
|
|
|
|
2026-08-07 14:38:16 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 处理 handleNodeDragStop 对应的数据或交互;作用范围仅限当前组件或模块。
|
|
|
|
|
|
* @param {*} payload 事件数据
|
|
|
|
|
|
*/
|
2026-07-29 11:43:07 +08:00
|
|
|
|
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();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-08-07 14:38:16 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 处理 handleNodesChange 对应的数据或交互;作用范围仅限当前组件或模块。
|
|
|
|
|
|
* @param {*} changes 变更集合
|
|
|
|
|
|
*/
|
2026-07-29 11:43:07 +08:00
|
|
|
|
const handleNodesChange = (changes) => {
|
|
|
|
|
|
if (changes.some((change) => ["add", "remove"].includes(change.type) || (change.type === "position" && change.dragging === false))) {
|
|
|
|
|
|
lf.markChanged();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-08-07 14:38:16 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 处理 handleEdgesChange 对应的数据或交互;作用范围仅限当前组件或模块。
|
|
|
|
|
|
* @param {*} changes 变更集合
|
|
|
|
|
|
*/
|
2026-07-29 11:43:07 +08:00
|
|
|
|
const handleEdgesChange = (changes) => {
|
|
|
|
|
|
if (changes.some((change) => ["add", "remove"].includes(change.type))) lf.markChanged();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-08-07 14:38:16 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 处理 handleEdgeClick 对应的数据或交互;作用范围仅限当前组件或模块。
|
|
|
|
|
|
* @param {*} { event 调用方传入的 { event 参数
|
|
|
|
|
|
* @param {*} edge } 调用方传入的 edge } 参数
|
|
|
|
|
|
*/
|
2026-07-29 11:43:07 +08:00
|
|
|
|
const handleEdgeClick = ({ event, edge }) => {
|
|
|
|
|
|
event?.stopPropagation?.();
|
|
|
|
|
|
lf.selectElementById(edge.id);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-08-07 14:38:16 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 处理 handlePaneClick 对应的数据或交互;作用范围仅限当前组件或模块。
|
|
|
|
|
|
*/
|
2026-07-29 11:43:07 +08:00
|
|
|
|
const handlePaneClick = () => lf.clearSelectElements();
|
|
|
|
|
|
|
2026-07-28 16:26:07 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-11 16:50:30 +08:00
|
|
|
|
const resourceValidation = validateFlowResources({ nodes, edges });
|
|
|
|
|
|
if (resourceValidation.errors.length > 0) {
|
|
|
|
|
|
const error = resourceValidation.errors[0];
|
|
|
|
|
|
ElMessage.warning(error.message);
|
|
|
|
|
|
if (error.nodeId) {
|
|
|
|
|
|
lf.selectElementById(error.nodeId);
|
|
|
|
|
|
lf.focusOn({ id: error.nodeId });
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-28 16:26:07 +08:00
|
|
|
|
isOpen.value = true;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 流程暂停
|
2026-08-07 14:38:16 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 处理 flowPauseFn 对应的数据或交互;作用范围仅限当前组件或模块。
|
|
|
|
|
|
*/
|
2026-07-28 16:26:07 +08:00
|
|
|
|
const flowPauseFn = async () => {
|
|
|
|
|
|
const res = await flowPause(instId.value);
|
|
|
|
|
|
if (res.code === 200) {
|
|
|
|
|
|
flowState.value = "pause";
|
|
|
|
|
|
ElMessage.success(res.msg);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const totalRunningTime = ref(0);
|
2026-07-29 16:17:39 +08:00
|
|
|
|
const hasRunResult = ref(false);
|
|
|
|
|
|
let flowPollTimer = null;
|
|
|
|
|
|
let flowDisposed = false;
|
|
|
|
|
|
|
2026-08-07 14:38:16 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 清理 clearFlowPollTimer 对应的数据或交互;作用范围仅限当前组件或模块。
|
|
|
|
|
|
*/
|
2026-07-29 16:17:39 +08:00
|
|
|
|
const clearFlowPollTimer = () => {
|
|
|
|
|
|
if (flowPollTimer !== null) {
|
|
|
|
|
|
window.clearTimeout(flowPollTimer);
|
|
|
|
|
|
flowPollTimer = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-08-07 14:38:16 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 调度 scheduleFlowView 对应的数据或交互;作用范围仅限当前组件或模块。
|
|
|
|
|
|
* @param {*} currentInstId 当前流程实例 ID
|
|
|
|
|
|
* @param {*} taskId 任务 ID
|
|
|
|
|
|
* @param {*} delay 延迟毫秒数
|
|
|
|
|
|
*/
|
2026-07-29 16:17:39 +08:00
|
|
|
|
const scheduleFlowView = (currentInstId, taskId = null, delay = 2000) => {
|
|
|
|
|
|
clearFlowPollTimer();
|
|
|
|
|
|
flowPollTimer = window.setTimeout(() => {
|
|
|
|
|
|
flowPollTimer = null;
|
|
|
|
|
|
if (!flowDisposed) loopFlowView(currentInstId, taskId);
|
|
|
|
|
|
}, delay);
|
|
|
|
|
|
};
|
2026-07-28 16:26:07 +08:00
|
|
|
|
|
2026-08-07 14:38:16 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 处理 flowResumeFn 对应的数据或交互;作用范围仅限当前组件或模块。
|
|
|
|
|
|
*/
|
2026-07-28 16:26:07 +08:00
|
|
|
|
const flowResumeFn = async () => {
|
|
|
|
|
|
const res = await flowResume(instId.value);
|
|
|
|
|
|
if (res.code === 200) {
|
|
|
|
|
|
flowState.value = "testRunning";
|
2026-07-29 16:17:39 +08:00
|
|
|
|
clearFlowPollTimer();
|
2026-07-28 16:26:07 +08:00
|
|
|
|
loopFlowView(instId.value);
|
|
|
|
|
|
ElMessage.success(res.msg);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-08-07 14:38:16 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 处理 flowStopFn 对应的数据或交互;作用范围仅限当前组件或模块。
|
|
|
|
|
|
*/
|
2026-07-28 16:26:07 +08:00
|
|
|
|
const flowStopFn = async () => {
|
|
|
|
|
|
const res = await flowStop(instId.value);
|
|
|
|
|
|
if (res.code === 200) {
|
2026-07-29 16:17:39 +08:00
|
|
|
|
clearFlowPollTimer();
|
2026-07-28 16:26:07 +08:00
|
|
|
|
flowState.value = "stop";
|
2026-07-29 16:17:39 +08:00
|
|
|
|
hasRunResult.value = true;
|
2026-07-28 16:26:07 +08:00
|
|
|
|
flowStore.updateDisableForm(false);
|
|
|
|
|
|
ElMessage.success(res.msg);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const initFlowData = {
|
|
|
|
|
|
nodes: [
|
|
|
|
|
|
{
|
|
|
|
|
|
type: "start",
|
|
|
|
|
|
x: 300,
|
|
|
|
|
|
y: 100,
|
|
|
|
|
|
properties: {
|
|
|
|
|
|
name: "Start",
|
|
|
|
|
|
action: "start",
|
2026-08-11 16:50:30 +08:00
|
|
|
|
schemaVersion: 2,
|
|
|
|
|
|
resourceRoles: [createDefaultRobotRole()],
|
|
|
|
|
|
inputParams: [],
|
2026-07-28 16:26:07 +08:00
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
type: "end",
|
|
|
|
|
|
x: 1300,
|
|
|
|
|
|
y: 700,
|
|
|
|
|
|
properties: {
|
|
|
|
|
|
name: "End",
|
|
|
|
|
|
action: "end",
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
// 边
|
|
|
|
|
|
edges: [],
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-07-29 16:17:39 +08:00
|
|
|
|
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
|
|
|
|
|
|
));
|
|
|
|
|
|
|
2026-07-28 16:26:07 +08:00
|
|
|
|
const validEdge = (data) => {
|
2026-08-07 14:38:16 +08:00
|
|
|
|
// 校验思路:先确认端点存在,再按节点类型限制方向;最后结合锚点名称和现有连线判重。
|
|
|
|
|
|
// 任一阶段失败都会删除候选连线,避免非法边进入可保存的画布数据。
|
2026-07-29 16:17:39 +08:00
|
|
|
|
const sourceModel = lf.getNodeModelById(data.sourceNodeId);
|
|
|
|
|
|
const targetModel = lf.getNodeModelById(data.targetNodeId);
|
|
|
|
|
|
if (!sourceModel || !targetModel) {
|
|
|
|
|
|
lf.deleteEdge(data.id);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const sourceAnchorArr = sourceModel.anchors;
|
2026-07-28 16:26:07 +08:00
|
|
|
|
const [sourceName] = sourceAnchorArr
|
|
|
|
|
|
.filter((item) => {
|
|
|
|
|
|
return item.id === data.sourceAnchorId;
|
|
|
|
|
|
})
|
|
|
|
|
|
.map((item) => {
|
|
|
|
|
|
return item.name;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-07-29 16:17:39 +08:00
|
|
|
|
const targetAnchorArr = targetModel.anchors;
|
2026-07-28 16:26:07 +08:00
|
|
|
|
const [targetName] = targetAnchorArr
|
|
|
|
|
|
.filter((item) => {
|
|
|
|
|
|
return item.id === data.targetAnchorId;
|
|
|
|
|
|
})
|
|
|
|
|
|
.map((item) => {
|
|
|
|
|
|
return item.name;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-07-29 16:17:39 +08:00
|
|
|
|
if (!sourceName || !targetName) {
|
|
|
|
|
|
lf.deleteEdge(data.id);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-28 16:26:07 +08:00
|
|
|
|
if (sourceName === "right" && targetName === "left") {
|
2026-08-07 10:48:18 +08:00
|
|
|
|
if (hasSameEdge(data, data.id)) {
|
|
|
|
|
|
lf.deleteEdge(data.id);
|
|
|
|
|
|
}
|
2026-07-28 16:26:07 +08:00
|
|
|
|
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);
|
2026-07-29 16:17:39 +08:00
|
|
|
|
if (hasSameEdge({ sourceNodeId, targetNodeId, sourceAnchorId, targetAnchorId })) return;
|
2026-07-28 16:26:07 +08:00
|
|
|
|
lf.addEdge({
|
|
|
|
|
|
type: "bezier",
|
|
|
|
|
|
sourceNodeId,
|
|
|
|
|
|
targetNodeId,
|
|
|
|
|
|
sourceAnchorId,
|
|
|
|
|
|
targetAnchorId,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const instId = ref(null);
|
|
|
|
|
|
const changeState = (value) => {
|
2026-08-07 10:48:18 +08:00
|
|
|
|
oldFlowData = JSON.parse(JSON.stringify(lf.getGraphData()));
|
2026-07-28 16:26:07 +08:00
|
|
|
|
isOpen.value = false;
|
|
|
|
|
|
flowStore.updateDisableForm(true);
|
|
|
|
|
|
flowState.value = value.type;
|
2026-07-29 16:17:39 +08:00
|
|
|
|
hasRunResult.value = false;
|
2026-07-28 16:26:07 +08:00
|
|
|
|
|
|
|
|
|
|
instId.value = value.instId;
|
2026-07-29 16:17:39 +08:00
|
|
|
|
clearFlowPollTimer();
|
2026-07-28 16:26:07 +08:00
|
|
|
|
loopFlowView(value.instId);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const loopFlowView = async (instId, taskId = null) => {
|
2026-08-07 14:38:16 +08:00
|
|
|
|
// 轮询思路:优先使用任务 ID 查询指定执行,否则查询实例最新状态,再统一映射节点状态。
|
|
|
|
|
|
// 仅在非日志模式下根据终态停止或续排定时器,日志回放保持为一次性快照。
|
2026-07-28 16:26:07 +08:00
|
|
|
|
const params = {
|
|
|
|
|
|
itemId: flowInfoData.value.itemId,
|
|
|
|
|
|
instId,
|
|
|
|
|
|
};
|
|
|
|
|
|
if (taskId) {
|
|
|
|
|
|
params.taskId = taskId;
|
|
|
|
|
|
}
|
|
|
|
|
|
const res = await flowView(params);
|
2026-07-29 16:17:39 +08:00
|
|
|
|
if (flowDisposed) return;
|
2026-07-28 16:26:07 +08:00
|
|
|
|
if (res.code === 200) {
|
|
|
|
|
|
const arr = res.data || [];
|
2026-07-29 16:17:39 +08:00
|
|
|
|
if (arr.length > 0) hasRunResult.value = true;
|
2026-07-28 16:26:07 +08:00
|
|
|
|
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)
|
|
|
|
|
|
) {
|
2026-07-29 16:17:39 +08:00
|
|
|
|
scheduleFlowView(instId);
|
2026-07-28 16:26:07 +08:00
|
|
|
|
} 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) {
|
2026-08-11 16:50:30 +08:00
|
|
|
|
const flowData = normalizeFlowResourceSchema(JSON.parse(res.data.flowData));
|
2026-07-28 16:26:07 +08:00
|
|
|
|
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);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-08-07 10:48:18 +08:00
|
|
|
|
const handleBeforeUnload = (_event) => {
|
2026-07-28 16:26:07 +08:00
|
|
|
|
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);
|
2026-07-29 16:17:39 +08:00
|
|
|
|
scheduleFlowView(flowInfoData.value.instId, flowInfoData.value.taskId, 1000);
|
2026-07-28 16:26:07 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
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) => {
|
2026-07-29 16:17:39 +08:00
|
|
|
|
const nodes = selectedNodes.filter((item) => (
|
|
|
|
|
|
!["selectArea", "loop"].includes(item.type)
|
|
|
|
|
|
&& !lf.getNodeModelById(item.id)?.parentId
|
|
|
|
|
|
));
|
|
|
|
|
|
if (nodes.length < 2) {
|
|
|
|
|
|
ElMessage.warning("请至少选择两个未分组节点");
|
2026-07-29 11:43:07 +08:00
|
|
|
|
groupSelectionPending.value = false;
|
|
|
|
|
|
return;
|
2026-07-28 16:26:07 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-29 16:17:39 +08:00
|
|
|
|
const INSET = { left: 52, top: 120, right: 52, bottom: 52 };
|
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;
|
2026-07-29 16:17:39 +08:00
|
|
|
|
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);
|
2026-07-29 11:43:07 +08:00
|
|
|
|
children.push(item.id);
|
2026-07-28 16:26:07 +08:00
|
|
|
|
});
|
2026-07-29 11:43:07 +08:00
|
|
|
|
|
2026-07-29 16:17:39 +08:00
|
|
|
|
const width = maxX - minX + INSET.left + INSET.right;
|
|
|
|
|
|
const height = maxY - minY + INSET.top + INSET.bottom;
|
|
|
|
|
|
lf.groupNodes({
|
2026-07-29 11:43:07 +08:00
|
|
|
|
type: "selectArea",
|
2026-07-29 16:17:39 +08:00
|
|
|
|
x: minX - INSET.left + width / 2,
|
|
|
|
|
|
y: minY - INSET.top + height / 2,
|
2026-07-29 11:43:07 +08:00
|
|
|
|
properties: {
|
|
|
|
|
|
name: "分组",
|
|
|
|
|
|
action: "NONE",
|
2026-07-29 16:17:39 +08:00
|
|
|
|
width,
|
|
|
|
|
|
height,
|
2026-07-29 11:43:07 +08:00
|
|
|
|
},
|
2026-07-29 16:17:39 +08:00
|
|
|
|
}, children);
|
2026-07-29 11:43:07 +08:00
|
|
|
|
lf.clearSelectElements();
|
|
|
|
|
|
groupSelectionPending.value = false;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const group = () => {
|
2026-07-29 16:17:39 +08:00
|
|
|
|
if (groupSelectionPending.value) {
|
|
|
|
|
|
groupSelectionPending.value = false;
|
|
|
|
|
|
lf.clearSelectElements();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-07-29 11:43:07 +08:00
|
|
|
|
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();
|
2026-08-07 10:48:18 +08:00
|
|
|
|
treeData.value = convertToTree(nodes);
|
2026-07-28 16:26:07 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
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("");
|
2026-08-07 10:48:18 +08:00
|
|
|
|
const singleNodeExecuting = ref(false);
|
2026-08-07 11:28:01 +08:00
|
|
|
|
const activeSingleNodeId = ref("");
|
|
|
|
|
|
// 单节点机器人选择按节点缓存;新节点默认沿用最近一次选择。
|
|
|
|
|
|
const singleNodeRobotCache = new Map();
|
|
|
|
|
|
let lastSingleNodeRobotId = "";
|
2026-08-04 11:46:14 +08:00
|
|
|
|
const stationLoading = ref(false);
|
2026-08-04 17:29:07 +08:00
|
|
|
|
const singleNodeRobotOptions = ref([]);
|
2026-08-07 10:48:18 +08:00
|
|
|
|
const flattenSingleNodeParams = (params, parentPath = [], fields = []) => {
|
|
|
|
|
|
(params || []).forEach((param, index) => {
|
|
|
|
|
|
const name = String(param?.name || "").trim();
|
|
|
|
|
|
const path = [...parentPath, name || `param-${index}`];
|
|
|
|
|
|
if (Array.isArray(param?.children)) {
|
|
|
|
|
|
flattenSingleNodeParams(param.children, path, fields);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!name && (param?.input === "" || param?.input === null || param?.input === undefined)) return;
|
|
|
|
|
|
fields.push({ key: `${path.join(".")}-${index}`, param });
|
|
|
|
|
|
});
|
|
|
|
|
|
return fields;
|
|
|
|
|
|
};
|
|
|
|
|
|
const singleNodeFields = computed(() => flattenSingleNodeParams(formData.nodeParams));
|
|
|
|
|
|
const buildSingleNodePayload = (params) => {
|
|
|
|
|
|
const payload = {};
|
|
|
|
|
|
(params || []).forEach((param) => {
|
|
|
|
|
|
const name = String(param?.name || "").trim();
|
|
|
|
|
|
if (!name) return;
|
|
|
|
|
|
payload[name] = Array.isArray(param.children)
|
|
|
|
|
|
? buildSingleNodePayload(param.children)
|
|
|
|
|
|
: param.input;
|
|
|
|
|
|
});
|
|
|
|
|
|
return payload;
|
|
|
|
|
|
};
|
|
|
|
|
|
const validateSingleNodeParams = () => {
|
|
|
|
|
|
for (const { param } of singleNodeFields.value) {
|
|
|
|
|
|
if (!String(param?.name || "").trim()) {
|
|
|
|
|
|
ElMessage.warning("请输入参数名");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (param.input === "" || param.input === null || param.input === undefined) {
|
|
|
|
|
|
ElMessage.warning(`请输入参数 ${param.name} 的值`);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
};
|
2026-08-04 17:29:07 +08:00
|
|
|
|
const singleNodeRobotId = computed(() =>
|
|
|
|
|
|
formData.nodeParams.find((item) => item.name === "robotId")?.input
|
2026-08-04 11:46:14 +08:00
|
|
|
|
);
|
|
|
|
|
|
const singleNodeDeviceId = computed(() =>
|
|
|
|
|
|
formData.nodeParams.find((item) => item.name === "deviceId")?.input
|
|
|
|
|
|
);
|
2026-08-07 11:28:01 +08:00
|
|
|
|
// 在执行、关闭弹窗或切换节点前记录当前机器人选择。
|
|
|
|
|
|
const rememberSingleNodeRobot = () => {
|
|
|
|
|
|
if (!activeSingleNodeId.value || !singleNodeRobotId.value) return;
|
|
|
|
|
|
lastSingleNodeRobotId = singleNodeRobotId.value;
|
|
|
|
|
|
singleNodeRobotCache.set(activeSingleNodeId.value, singleNodeRobotId.value);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
watch(visible, (value) => {
|
|
|
|
|
|
if (!value) rememberSingleNodeRobot();
|
|
|
|
|
|
});
|
2026-08-04 11:46:14 +08:00
|
|
|
|
|
2026-08-05 09:27:55 +08:00
|
|
|
|
const handleSingleNodeRobotChange = async (robotId) => {
|
|
|
|
|
|
const deviceParam = formData.nodeParams.find((item) => item.name === "deviceId");
|
|
|
|
|
|
if (!deviceParam) return;
|
2026-08-05 10:46:11 +08:00
|
|
|
|
const originalDeviceId = deviceParam.input;
|
|
|
|
|
|
const originalOptions = [...(deviceParam.selectOptions || [])];
|
|
|
|
|
|
const originalLocked = Boolean(deviceParam.deviceLocked);
|
2026-08-05 09:27:55 +08:00
|
|
|
|
const stationParam = formData.nodeParams.find((item) => item.name === "stationId");
|
|
|
|
|
|
if (!robotId) return;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const deviceKind = getDeviceKindForAction(nodeAction.value);
|
|
|
|
|
|
const response = await getRobotDevicesByRobotId(robotId,
|
|
|
|
|
|
deviceKind ? { deviceKind } : {});
|
|
|
|
|
|
const devices = Array.isArray(response.data) ? response.data : [];
|
2026-08-05 10:46:11 +08:00
|
|
|
|
if (!devices.length) {
|
|
|
|
|
|
deviceParam.input = originalDeviceId;
|
|
|
|
|
|
deviceParam.selectOptions = originalOptions;
|
|
|
|
|
|
deviceParam.deviceLocked = originalLocked;
|
|
|
|
|
|
ElMessage.warning("当前机器人没有匹配的在线设备,已保留原设备");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (stationParam) {
|
|
|
|
|
|
stationParam.input = "";
|
|
|
|
|
|
stationParam.selectOptions = [];
|
|
|
|
|
|
}
|
|
|
|
|
|
deviceParam.componentType = "select";
|
2026-08-05 09:27:55 +08:00
|
|
|
|
deviceParam.selectOptions = toDeviceOptions(devices);
|
2026-08-11 16:50:30 +08:00
|
|
|
|
const currentDevice = devices.find((device) => device.deviceId === originalDeviceId);
|
|
|
|
|
|
deviceParam.input = (currentDevice || devices[0]).deviceId;
|
|
|
|
|
|
deviceParam.deviceLocked = devices.length === 1;
|
2026-08-05 09:27:55 +08:00
|
|
|
|
} catch (error) {
|
2026-08-05 10:46:11 +08:00
|
|
|
|
deviceParam.input = originalDeviceId;
|
|
|
|
|
|
deviceParam.selectOptions = originalOptions;
|
|
|
|
|
|
deviceParam.deviceLocked = originalLocked;
|
2026-08-05 09:27:55 +08:00
|
|
|
|
console.error("加载机器人设备失败:", error);
|
|
|
|
|
|
ElMessage.error("加载机器人设备失败");
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-08-07 14:38:16 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 处理 stationOptions 对应的数据或交互;作用范围仅限当前组件或模块。
|
|
|
|
|
|
* @param {*} stations 站点集合
|
|
|
|
|
|
*/
|
2026-08-04 11:46:14 +08:00
|
|
|
|
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 };
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-08-07 14:38:16 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 加载 loadSingleNodeStations 对应的数据或交互;作用范围仅限当前组件或模块。
|
|
|
|
|
|
*/
|
2026-08-04 11:46:14 +08:00
|
|
|
|
const loadSingleNodeStations = async () => {
|
|
|
|
|
|
stationLoading.value = true;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await getAgvStations({
|
2026-08-04 17:29:07 +08:00
|
|
|
|
robotId: singleNodeRobotId.value,
|
2026-08-04 11:46:14 +08:00
|
|
|
|
deviceId: singleNodeDeviceId.value,
|
|
|
|
|
|
});
|
|
|
|
|
|
const options = stationOptions(response?.data);
|
|
|
|
|
|
const stationParam = formData.nodeParams.find((item) => item.name === "stationId");
|
|
|
|
|
|
if (!stationParam) return;
|
|
|
|
|
|
stationParam.selectOptions = options;
|
2026-08-04 17:29:07 +08:00
|
|
|
|
stationParam.stationRobotId = singleNodeRobotId.value;
|
2026-08-04 11:46:14 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2026-07-28 16:26:07 +08:00
|
|
|
|
|
2026-08-07 14:38:16 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 加载 loadSingleNodeRobots 对应的数据或交互;作用范围仅限当前组件或模块。
|
|
|
|
|
|
*/
|
2026-08-04 17:29:07 +08:00
|
|
|
|
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,
|
|
|
|
|
|
}));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-08-07 14:38:16 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 处理 singleNodeExecution 对应的数据或交互;作用范围仅限当前组件或模块。
|
|
|
|
|
|
* @param {*} e 触发操作的浏览器事件
|
|
|
|
|
|
*/
|
2026-08-04 17:29:07 +08:00
|
|
|
|
const singleNodeExecution = async (e) => {
|
2026-08-07 11:28:01 +08:00
|
|
|
|
rememberSingleNodeRobot();
|
|
|
|
|
|
const { name, nodeType, nodeParams, action, nodeId } = e.detail;
|
2026-08-11 16:50:30 +08:00
|
|
|
|
const executionParams = JSON.parse(JSON.stringify(nodeParams || []))
|
|
|
|
|
|
.filter((param) => !['robotId', 'deviceId'].includes(param.name));
|
2026-08-07 10:48:18 +08:00
|
|
|
|
const normalizedAction = String(action || "").toUpperCase();
|
|
|
|
|
|
const effectiveNodeType = nodeType || (getDeviceKindForAction(normalizedAction) ? "EDGE" : "");
|
2026-08-07 11:28:01 +08:00
|
|
|
|
activeSingleNodeId.value = nodeId || `${normalizedAction}:${name || ""}`;
|
2026-07-28 16:26:07 +08:00
|
|
|
|
nodeName.value = name;
|
|
|
|
|
|
visible.value = true;
|
2026-08-07 10:48:18 +08:00
|
|
|
|
nodeAction.value = normalizedAction;
|
|
|
|
|
|
singleNodeExecuting.value = false;
|
2026-08-04 11:46:14 +08:00
|
|
|
|
stationLoading.value = false;
|
2026-08-07 10:48:18 +08:00
|
|
|
|
if (effectiveNodeType === "EDGE") {
|
2026-08-04 17:29:07 +08:00
|
|
|
|
await loadSingleNodeRobots();
|
2026-08-11 16:50:30 +08:00
|
|
|
|
const routingParams = [
|
2026-07-28 16:26:07 +08:00
|
|
|
|
{
|
|
|
|
|
|
disabled: true,
|
|
|
|
|
|
input: "",
|
2026-08-04 17:29:07 +08:00
|
|
|
|
name: "robotId",
|
2026-07-28 16:26:07 +08:00
|
|
|
|
type: "input",
|
2026-08-04 17:29:07 +08:00
|
|
|
|
componentType: "select",
|
|
|
|
|
|
selectOptions: singleNodeRobotOptions.value,
|
2026-07-28 16:26:07 +08:00
|
|
|
|
},
|
|
|
|
|
|
];
|
2026-08-11 16:50:30 +08:00
|
|
|
|
if (actionRequiresDevice(normalizedAction)) {
|
|
|
|
|
|
routingParams.push({
|
|
|
|
|
|
disabled: true,
|
|
|
|
|
|
input: "",
|
|
|
|
|
|
name: "deviceId",
|
|
|
|
|
|
type: "input",
|
|
|
|
|
|
componentType: "select",
|
|
|
|
|
|
selectOptions: [],
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
formData.nodeParams = [...routingParams, ...executionParams];
|
2026-08-07 11:28:01 +08:00
|
|
|
|
const cachedRobotId = singleNodeRobotCache.get(activeSingleNodeId.value) || lastSingleNodeRobotId;
|
|
|
|
|
|
if (cachedRobotId) {
|
|
|
|
|
|
formData.nodeParams[0].input = cachedRobotId;
|
|
|
|
|
|
await handleSingleNodeRobotChange(cachedRobotId);
|
|
|
|
|
|
}
|
2026-07-28 16:26:07 +08:00
|
|
|
|
} else {
|
2026-08-04 11:46:14 +08:00
|
|
|
|
formData.nodeParams = executionParams;
|
|
|
|
|
|
}
|
2026-08-07 10:48:18 +08:00
|
|
|
|
if (nodeAction.value === "AGV_MOVE_TO_STATION") {
|
2026-08-04 11:46:14 +08:00
|
|
|
|
const stationParam = formData.nodeParams.find((item) => item.name === "stationId");
|
|
|
|
|
|
if (stationParam) {
|
|
|
|
|
|
stationParam.input = "";
|
|
|
|
|
|
stationParam.selectOptions = [];
|
|
|
|
|
|
}
|
2026-07-28 16:26:07 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-08-07 14:38:16 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 执行 execute 对应的数据或交互;作用范围仅限当前组件或模块。
|
|
|
|
|
|
*/
|
2026-07-28 16:26:07 +08:00
|
|
|
|
const execute = async () => {
|
2026-08-04 11:46:14 +08:00
|
|
|
|
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);
|
2026-08-04 17:29:07 +08:00
|
|
|
|
const correctSource = stationParam?.stationRobotId === singleNodeRobotId.value
|
2026-08-04 11:46:14 +08:00
|
|
|
|
&& stationParam?.stationDeviceId === singleNodeDeviceId.value;
|
|
|
|
|
|
if (!validStation || !correctSource) {
|
2026-08-05 09:27:55 +08:00
|
|
|
|
ElMessage.warning("请按当前机器人和设备重新加载并选择站点");
|
2026-08-04 11:46:14 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-08-07 10:48:18 +08:00
|
|
|
|
if (!validateSingleNodeParams()) return;
|
2026-08-07 11:28:01 +08:00
|
|
|
|
rememberSingleNodeRobot();
|
2026-08-07 10:48:18 +08:00
|
|
|
|
singleNodeExecuting.value = true;
|
|
|
|
|
|
try {
|
2026-07-28 16:26:07 +08:00
|
|
|
|
const res = await flowAction({
|
|
|
|
|
|
action: nodeAction.value,
|
2026-08-07 10:48:18 +08:00
|
|
|
|
payload: buildSingleNodePayload(formData.nodeParams),
|
2026-07-28 16:26:07 +08:00
|
|
|
|
});
|
|
|
|
|
|
if (res.code === 200) {
|
|
|
|
|
|
ElMessage.success("执行成功");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ElMessage.error(res.msg);
|
|
|
|
|
|
}
|
2026-08-07 10:48:18 +08:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("单节点执行失败:", error);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
singleNodeExecuting.value = false;
|
2026-07-28 16:26:07 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const showParamsDrawer = ref(false);
|
|
|
|
|
|
const paramsDrawerData = ref({});
|
|
|
|
|
|
|
2026-08-07 14:38:16 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 清理 clearRun 对应的数据或交互;作用范围仅限当前组件或模块。
|
|
|
|
|
|
*/
|
2026-07-28 16:26:07 +08:00
|
|
|
|
const clearRun = () => {
|
|
|
|
|
|
const { nodes } = lf.getGraphData();
|
|
|
|
|
|
nodes.forEach((item) => {
|
|
|
|
|
|
const node = lf.getNodeModelById(item.id);
|
|
|
|
|
|
if (node.closePopover) {
|
|
|
|
|
|
node.closePopover();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2026-07-29 16:17:39 +08:00
|
|
|
|
hasRunResult.value = false;
|
|
|
|
|
|
totalRunningTime.value = 0;
|
2026-07-28 16:26:07 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-04 10:08:57 +08:00
|
|
|
|
const NODE_PASTE_OFFSET = 40;
|
|
|
|
|
|
const NON_COPYABLE_NODE_TYPES = new Set(["start", "end", "selectArea"]);
|
2026-08-11 16:50:30 +08:00
|
|
|
|
const FLOW_CLIPBOARD_TYPE = "cmvr-flow/nodes";
|
|
|
|
|
|
const FLOW_CLIPBOARD_VERSION = 1;
|
|
|
|
|
|
const FLOW_CLIPBOARD_MIME = "application/x-cmvr-flow-nodes";
|
|
|
|
|
|
const FLOW_CLIPBOARD_STORAGE_KEY = "cmvr-flow:clipboard";
|
|
|
|
|
|
const FLOW_EDITOR_INSTANCE_ID = uuid();
|
|
|
|
|
|
const NODE_REFERENCE_ARRAY_KEYS = new Set(["quote", "nameQuote"]);
|
|
|
|
|
|
let copiedPayload = null;
|
|
|
|
|
|
let lastPastedClipboardId = "";
|
2026-08-04 10:08:57 +08:00
|
|
|
|
let pasteCount = 0;
|
2026-07-29 11:43:07 +08:00
|
|
|
|
|
2026-08-05 10:15:34 +08:00
|
|
|
|
const EDITABLE_SELECTOR = [
|
|
|
|
|
|
"input",
|
|
|
|
|
|
"textarea",
|
|
|
|
|
|
"select",
|
|
|
|
|
|
"[contenteditable]:not([contenteditable='false'])",
|
|
|
|
|
|
".monaco-editor",
|
|
|
|
|
|
".el-input",
|
|
|
|
|
|
".el-textarea",
|
|
|
|
|
|
".el-select",
|
|
|
|
|
|
".el-cascader",
|
|
|
|
|
|
".el-input-number",
|
|
|
|
|
|
"[role='dialog']",
|
|
|
|
|
|
].join(",");
|
|
|
|
|
|
|
2026-08-07 14:38:16 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 判断 isEditableElement 对应的数据或交互;作用范围仅限当前组件或模块。
|
|
|
|
|
|
* @param {*} element 调用方传入的 element 参数
|
|
|
|
|
|
*/
|
2026-08-05 10:15:34 +08:00
|
|
|
|
const isEditableElement = (element) => (
|
|
|
|
|
|
element instanceof Element && Boolean(element.closest(EDITABLE_SELECTOR))
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-08-07 14:38:16 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 判断 isTextEditing 对应的数据或交互;作用范围仅限当前组件或模块。
|
|
|
|
|
|
* @param {*} event 触发操作的浏览器事件
|
|
|
|
|
|
*/
|
2026-08-04 10:08:57 +08:00
|
|
|
|
const isTextEditing = (event) => {
|
2026-08-05 10:15:34 +08:00
|
|
|
|
const eventPath = typeof event.composedPath === "function" ? event.composedPath() : [];
|
|
|
|
|
|
if (eventPath.some(isEditableElement)) return true;
|
|
|
|
|
|
if (isEditableElement(event.target) || isEditableElement(document.activeElement)) return true;
|
|
|
|
|
|
return Boolean(window.getSelection()?.toString());
|
2026-08-04 10:08:57 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-08-07 14:38:16 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 复制 copySelectedNodes 对应的数据或交互;作用范围仅限当前组件或模块。
|
|
|
|
|
|
* @param {*} event 触发操作的浏览器事件
|
|
|
|
|
|
*/
|
2026-08-04 10:08:57 +08:00
|
|
|
|
const copySelectedNodes = (event) => {
|
2026-08-07 14:38:16 +08:00
|
|
|
|
// 复制只保留选中节点及它们之间的内部连线,避免粘贴时重建指向未选节点的关系。
|
|
|
|
|
|
// 节点数据在此深拷贝,后续生成新 ID 时不会反向污染原画布对象。
|
2026-08-11 16:50:30 +08:00
|
|
|
|
if (flowStore.disableForm || isTextEditing(event)) return;
|
2026-08-05 15:18:52 +08:00
|
|
|
|
const selectedNodes = lf.getSelectElements().nodes;
|
|
|
|
|
|
if (!selectedNodes.length) return;
|
|
|
|
|
|
|
|
|
|
|
|
const graph = lf.getGraphData();
|
|
|
|
|
|
const nodeById = new Map(graph.nodes.map((node) => [node.id, node]));
|
|
|
|
|
|
const selectedIds = new Set(selectedNodes.map((node) => node.id));
|
2026-08-07 14:38:16 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 新增 addChildren 对应的数据或交互;作用范围仅限当前组件或模块。
|
|
|
|
|
|
* @param {*} nodeId 流程节点 ID
|
|
|
|
|
|
*/
|
2026-08-05 15:18:52 +08:00
|
|
|
|
const addChildren = (nodeId) => {
|
|
|
|
|
|
const node = nodeById.get(nodeId);
|
|
|
|
|
|
(node?.children || []).forEach((childId) => {
|
|
|
|
|
|
if (selectedIds.has(childId)) return;
|
|
|
|
|
|
selectedIds.add(childId);
|
|
|
|
|
|
addChildren(childId);
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
[...selectedIds].forEach(addChildren);
|
|
|
|
|
|
|
|
|
|
|
|
const copyableIds = new Set(
|
|
|
|
|
|
[...selectedIds].filter((id) => {
|
|
|
|
|
|
const node = nodeById.get(id);
|
|
|
|
|
|
return node && !NON_COPYABLE_NODE_TYPES.has(node.type);
|
|
|
|
|
|
}),
|
2026-08-04 10:08:57 +08:00
|
|
|
|
);
|
2026-08-05 15:18:52 +08:00
|
|
|
|
const nodes = graph.nodes.filter((node) => copyableIds.has(node.id));
|
2026-08-04 10:08:57 +08:00
|
|
|
|
if (!nodes.length) return;
|
2026-08-05 15:18:52 +08:00
|
|
|
|
const edges = graph.edges.filter((edge) => (
|
|
|
|
|
|
copyableIds.has(edge.sourceNodeId) && copyableIds.has(edge.targetNodeId)
|
|
|
|
|
|
));
|
2026-08-04 10:08:57 +08:00
|
|
|
|
|
2026-08-11 16:50:30 +08:00
|
|
|
|
const payload = {
|
|
|
|
|
|
type: FLOW_CLIPBOARD_TYPE,
|
|
|
|
|
|
version: FLOW_CLIPBOARD_VERSION,
|
|
|
|
|
|
clipboardId: uuid(),
|
|
|
|
|
|
sourceEditorId: FLOW_EDITOR_INSTANCE_ID,
|
|
|
|
|
|
sourceFlowId: flowInfoData.value.itemId || null,
|
|
|
|
|
|
copiedAt: Date.now(),
|
|
|
|
|
|
resourceRoles: getResourceRoles(graph),
|
|
|
|
|
|
graph: JSON.parse(JSON.stringify({ nodes, edges })),
|
|
|
|
|
|
};
|
|
|
|
|
|
const serialized = JSON.stringify(payload);
|
|
|
|
|
|
|
2026-08-04 10:08:57 +08:00
|
|
|
|
event.preventDefault();
|
2026-08-11 16:50:30 +08:00
|
|
|
|
if (event.clipboardData) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
event.clipboardData.setData(FLOW_CLIPBOARD_MIME, serialized);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.debug("浏览器不支持自定义流程剪贴板类型,使用文本格式", error);
|
|
|
|
|
|
}
|
|
|
|
|
|
event.clipboardData.setData("text/plain", serialized);
|
|
|
|
|
|
}
|
|
|
|
|
|
copiedPayload = payload;
|
|
|
|
|
|
try {
|
|
|
|
|
|
localStorage.setItem(FLOW_CLIPBOARD_STORAGE_KEY, serialized);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.debug("流程剪贴板本地缓存写入失败", error);
|
|
|
|
|
|
}
|
2026-08-04 10:08:57 +08:00
|
|
|
|
pasteCount = 0;
|
2026-08-11 16:50:30 +08:00
|
|
|
|
lastPastedClipboardId = "";
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 解析并校验流程剪贴板内容,避免把普通 JSON 文本当作流程节点粘贴。
|
|
|
|
|
|
* @param {*} raw 剪贴板原始文本
|
|
|
|
|
|
*/
|
|
|
|
|
|
const parseFlowClipboardPayload = (raw) => {
|
|
|
|
|
|
if (!raw || typeof raw !== "string") return null;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const payload = JSON.parse(raw);
|
|
|
|
|
|
if (
|
|
|
|
|
|
payload?.type !== FLOW_CLIPBOARD_TYPE
|
|
|
|
|
|
|| payload?.version !== FLOW_CLIPBOARD_VERSION
|
|
|
|
|
|
|| !Array.isArray(payload?.graph?.nodes)
|
|
|
|
|
|
|| !Array.isArray(payload?.graph?.edges)
|
|
|
|
|
|
|| payload.graph.nodes.length === 0
|
|
|
|
|
|
) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
return payload;
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 优先读取系统剪贴板;浏览器未暴露剪贴板数据时使用同源窗口共享缓存。
|
|
|
|
|
|
* @param {*} event 粘贴事件
|
|
|
|
|
|
*/
|
|
|
|
|
|
const readFlowClipboardPayload = (event) => {
|
|
|
|
|
|
const clipboard = event.clipboardData;
|
|
|
|
|
|
if (clipboard) {
|
|
|
|
|
|
const customText = clipboard.getData(FLOW_CLIPBOARD_MIME);
|
|
|
|
|
|
const plainText = clipboard.getData("text/plain");
|
|
|
|
|
|
const raw = customText || plainText;
|
|
|
|
|
|
// 系统剪贴板存在其他内容时,不应使用一份过期的本地流程缓存。
|
|
|
|
|
|
if (raw) return parseFlowClipboardPayload(raw);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (copiedPayload) return copiedPayload;
|
|
|
|
|
|
try {
|
|
|
|
|
|
return parseFlowClipboardPayload(localStorage.getItem(FLOW_CLIPBOARD_STORAGE_KEY));
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
2026-08-04 10:08:57 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-08-07 14:38:16 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 重映射 remapNodeAnchorId 对应的数据或交互;作用范围仅限当前组件或模块。
|
|
|
|
|
|
* @param {*} anchorId 锚点 ID
|
|
|
|
|
|
* @param {*} oldNodeId 原节点 ID
|
|
|
|
|
|
* @param {*} newNodeId 新节点 ID
|
|
|
|
|
|
*/
|
2026-08-05 15:18:52 +08:00
|
|
|
|
const remapNodeAnchorId = (anchorId, oldNodeId, newNodeId) => {
|
|
|
|
|
|
if (!anchorId || !String(anchorId).startsWith(`${oldNodeId}_`)) return anchorId;
|
|
|
|
|
|
return `${newNodeId}${String(anchorId).slice(oldNodeId.length)}`;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-08-07 14:38:16 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 重映射 remapCopiedNodeReferences 对应的数据或交互;作用范围仅限当前组件或模块。
|
|
|
|
|
|
* @param {*} value 待处理的新值
|
|
|
|
|
|
* @param {*} idMap 节点 ID 映射
|
|
|
|
|
|
* @param {*} parentKey 递归父键名
|
|
|
|
|
|
*/
|
2026-08-11 16:50:30 +08:00
|
|
|
|
const remapCopiedNodeReferences = (value, idMap, parentKey = "", options = {}) => {
|
2026-08-05 15:18:52 +08:00
|
|
|
|
if (Array.isArray(value)) {
|
2026-08-11 16:50:30 +08:00
|
|
|
|
const result = value.map((item) => remapCopiedNodeReferences(item, idMap, "", options));
|
|
|
|
|
|
if (NODE_REFERENCE_ARRAY_KEYS.has(parentKey) && result.length) {
|
|
|
|
|
|
if (idMap.has(result[0])) {
|
|
|
|
|
|
result[0] = idMap.get(result[0]);
|
|
|
|
|
|
} else if (options.dropExternalReferences) {
|
|
|
|
|
|
options.removedReferenceCount += 1;
|
|
|
|
|
|
return [];
|
|
|
|
|
|
}
|
2026-08-05 15:18:52 +08:00
|
|
|
|
}
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!value || typeof value !== "object") return value;
|
|
|
|
|
|
return Object.fromEntries(Object.entries(value).map(([key, item]) => (
|
2026-08-11 16:50:30 +08:00
|
|
|
|
[key, remapCopiedNodeReferences(item, idMap, key, options)]
|
2026-08-05 15:18:52 +08:00
|
|
|
|
)));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-08-11 16:50:30 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 跨窗口粘贴时将节点组移动到当前画布视口中心;当前窗口内复制仍使用固定偏移。
|
|
|
|
|
|
* @param {*} payload 流程剪贴板载荷
|
|
|
|
|
|
*/
|
|
|
|
|
|
const resolvePasteTranslation = (payload) => {
|
|
|
|
|
|
if (payload.sourceEditorId === FLOW_EDITOR_INSTANCE_ID) {
|
|
|
|
|
|
const offset = NODE_PASTE_OFFSET * pasteCount;
|
|
|
|
|
|
return { x: offset, y: offset };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const nodes = payload.graph.nodes;
|
|
|
|
|
|
const xs = nodes.map((node) => Number(node.x || 0));
|
|
|
|
|
|
const ys = nodes.map((node) => Number(node.y || 0));
|
|
|
|
|
|
const sourceCenter = {
|
|
|
|
|
|
x: (Math.min(...xs) + Math.max(...xs)) / 2,
|
|
|
|
|
|
y: (Math.min(...ys) + Math.max(...ys)) / 2,
|
|
|
|
|
|
};
|
|
|
|
|
|
const canvas = document.getElementById("main-flow");
|
|
|
|
|
|
if (!canvas) {
|
|
|
|
|
|
const offset = NODE_PASTE_OFFSET * pasteCount;
|
|
|
|
|
|
return { x: offset, y: offset };
|
|
|
|
|
|
}
|
|
|
|
|
|
const rect = canvas.getBoundingClientRect();
|
|
|
|
|
|
const target = lf.getPointByClient(rect.left + rect.width / 2, rect.top + rect.height / 2)
|
|
|
|
|
|
.canvasOverlayPosition;
|
|
|
|
|
|
const repeatedOffset = NODE_PASTE_OFFSET * (pasteCount - 1);
|
|
|
|
|
|
return {
|
|
|
|
|
|
x: target.x - sourceCenter.x + repeatedOffset,
|
|
|
|
|
|
y: target.y - sourceCenter.y + repeatedOffset,
|
|
|
|
|
|
};
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-08-07 14:38:16 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 粘贴 pasteCopiedNodes 对应的数据或交互;作用范围仅限当前组件或模块。
|
|
|
|
|
|
* @param {*} event 触发操作的浏览器事件
|
|
|
|
|
|
*/
|
2026-08-04 10:08:57 +08:00
|
|
|
|
const pasteCopiedNodes = (event) => {
|
2026-08-07 14:38:16 +08:00
|
|
|
|
// 先为全部节点建立旧 ID 到新 ID 的映射,再重建父子引用、锚点和内部连线。
|
|
|
|
|
|
// 该顺序保证循环、分组和分支之间的交叉引用都能解析到新的目标 ID。
|
2026-08-11 16:50:30 +08:00
|
|
|
|
if (flowStore.disableForm || isTextEditing(event)) return;
|
|
|
|
|
|
const payload = readFlowClipboardPayload(event);
|
|
|
|
|
|
if (!payload) return;
|
|
|
|
|
|
const copiedGraph = payload.graph;
|
2026-08-04 10:08:57 +08:00
|
|
|
|
|
|
|
|
|
|
event.preventDefault();
|
2026-08-11 16:50:30 +08:00
|
|
|
|
if (lastPastedClipboardId !== payload.clipboardId) {
|
|
|
|
|
|
pasteCount = 0;
|
|
|
|
|
|
lastPastedClipboardId = payload.clipboardId;
|
|
|
|
|
|
}
|
2026-08-04 10:08:57 +08:00
|
|
|
|
pasteCount += 1;
|
2026-08-11 16:50:30 +08:00
|
|
|
|
const translation = resolvePasteTranslation(payload);
|
2026-08-05 15:18:52 +08:00
|
|
|
|
const graph = lf.getGraphData();
|
|
|
|
|
|
const existingNodeIds = new Set(graph.nodes.map((node) => node.id));
|
|
|
|
|
|
const idMap = new Map(copiedGraph.nodes.map((node) => (
|
|
|
|
|
|
[node.id, uuid().replace(/-/g, "")]
|
|
|
|
|
|
)));
|
2026-08-11 16:50:30 +08:00
|
|
|
|
const isSameEditor = payload.sourceEditorId === FLOW_EDITOR_INSTANCE_ID;
|
|
|
|
|
|
const isSameFlow = Boolean(payload.sourceFlowId)
|
|
|
|
|
|
&& payload.sourceFlowId === flowInfoData.value.itemId;
|
|
|
|
|
|
const referenceOptions = {
|
|
|
|
|
|
dropExternalReferences: !isSameFlow,
|
|
|
|
|
|
removedReferenceCount: 0,
|
|
|
|
|
|
};
|
2026-08-05 15:18:52 +08:00
|
|
|
|
|
|
|
|
|
|
const pastedNodes = copiedGraph.nodes.map((sourceNode) => {
|
|
|
|
|
|
const newId = idMap.get(sourceNode.id);
|
|
|
|
|
|
const properties = remapCopiedNodeReferences(
|
|
|
|
|
|
JSON.parse(JSON.stringify(sourceNode.properties || {})),
|
|
|
|
|
|
idMap,
|
2026-08-11 16:50:30 +08:00
|
|
|
|
"",
|
|
|
|
|
|
referenceOptions,
|
2026-08-05 15:18:52 +08:00
|
|
|
|
);
|
2026-08-04 10:08:57 +08:00
|
|
|
|
const parentId = properties.parentId;
|
2026-08-05 15:18:52 +08:00
|
|
|
|
if (idMap.has(parentId)) properties.parentId = idMap.get(parentId);
|
2026-08-11 16:50:30 +08:00
|
|
|
|
else if (!isSameEditor || !existingNodeIds.has(parentId)) delete properties.parentId;
|
2026-08-05 15:18:52 +08:00
|
|
|
|
|
|
|
|
|
|
if (sourceNode.type === "branch" && Array.isArray(properties.anchor)) {
|
|
|
|
|
|
properties.anchor = properties.anchor.map((anchor) => ({
|
|
|
|
|
|
...anchor,
|
|
|
|
|
|
id: remapNodeAnchorId(anchor.id, sourceNode.id, newId),
|
|
|
|
|
|
}));
|
|
|
|
|
|
}
|
2026-08-04 10:08:57 +08:00
|
|
|
|
|
2026-08-05 15:18:52 +08:00
|
|
|
|
const children = (sourceNode.children || [])
|
|
|
|
|
|
.filter((childId) => idMap.has(childId))
|
|
|
|
|
|
.map((childId) => idMap.get(childId));
|
|
|
|
|
|
return {
|
|
|
|
|
|
...JSON.parse(JSON.stringify(sourceNode)),
|
|
|
|
|
|
id: newId,
|
2026-08-11 16:50:30 +08:00
|
|
|
|
x: Number(sourceNode.x || 0) + translation.x,
|
|
|
|
|
|
y: Number(sourceNode.y || 0) + translation.y,
|
2026-08-04 10:08:57 +08:00
|
|
|
|
properties,
|
2026-08-05 15:18:52 +08:00
|
|
|
|
children,
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
2026-08-04 10:08:57 +08:00
|
|
|
|
|
2026-08-05 15:18:52 +08:00
|
|
|
|
const pastedEdges = copiedGraph.edges.map((sourceEdge) => {
|
|
|
|
|
|
const sourceNodeId = idMap.get(sourceEdge.sourceNodeId);
|
|
|
|
|
|
const targetNodeId = idMap.get(sourceEdge.targetNodeId);
|
|
|
|
|
|
return {
|
|
|
|
|
|
...JSON.parse(JSON.stringify(sourceEdge)),
|
|
|
|
|
|
id: `edge_${uuid().replace(/-/g, "")}`,
|
|
|
|
|
|
sourceNodeId,
|
|
|
|
|
|
targetNodeId,
|
|
|
|
|
|
sourceAnchorId: remapNodeAnchorId(
|
|
|
|
|
|
sourceEdge.sourceAnchorId,
|
|
|
|
|
|
sourceEdge.sourceNodeId,
|
|
|
|
|
|
sourceNodeId,
|
|
|
|
|
|
),
|
|
|
|
|
|
targetAnchorId: remapNodeAnchorId(
|
|
|
|
|
|
sourceEdge.targetAnchorId,
|
|
|
|
|
|
sourceEdge.targetNodeId,
|
|
|
|
|
|
targetNodeId,
|
|
|
|
|
|
),
|
|
|
|
|
|
};
|
2026-08-04 10:08:57 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-08-11 16:50:30 +08:00
|
|
|
|
const resourceMerge = mergeCopiedResourceRoles(graph, payload.resourceRoles || [], pastedNodes);
|
|
|
|
|
|
const graphNodesWithResources = graph.nodes.map((node) => (
|
|
|
|
|
|
node.id === resourceMerge.start?.id ? resourceMerge.start : node
|
|
|
|
|
|
));
|
|
|
|
|
|
|
2026-08-05 15:18:52 +08:00
|
|
|
|
lf.render({
|
2026-08-11 16:50:30 +08:00
|
|
|
|
nodes: [...graphNodesWithResources, ...pastedNodes],
|
2026-08-05 15:18:52 +08:00
|
|
|
|
edges: [...graph.edges, ...pastedEdges],
|
|
|
|
|
|
}, {
|
|
|
|
|
|
resetHistory: false,
|
|
|
|
|
|
replaceNodes: true,
|
|
|
|
|
|
preserveNodeSizes: true,
|
|
|
|
|
|
preserveNodePositions: true,
|
|
|
|
|
|
});
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
lf.selectElementsByIds(pastedNodes.map((node) => node.id));
|
|
|
|
|
|
lf.markChanged();
|
2026-08-11 16:50:30 +08:00
|
|
|
|
if (referenceOptions.removedReferenceCount > 0) {
|
|
|
|
|
|
ElMessage.warning(
|
|
|
|
|
|
`已粘贴 ${pastedNodes.length} 个节点,${referenceOptions.removedReferenceCount} 个跨工作流参数引用已清除`,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (resourceMerge.addedRoles || resourceMerge.addedSlots) {
|
|
|
|
|
|
ElMessage.success(`已同步 ${resourceMerge.addedRoles} 个机器人角色和 ${resourceMerge.addedSlots} 个设备用途`);
|
|
|
|
|
|
}
|
2026-08-05 15:18:52 +08:00
|
|
|
|
});
|
2026-08-04 10:08:57 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-08-07 14:38:16 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 处理 handleKeyboardShortcut 对应的数据或交互;作用范围仅限当前组件或模块。
|
|
|
|
|
|
* @param {*} event 触发操作的浏览器事件
|
|
|
|
|
|
*/
|
2026-08-04 10:08:57 +08:00
|
|
|
|
const handleKeyboardShortcut = (event) => {
|
|
|
|
|
|
if (isTextEditing(event)) return;
|
|
|
|
|
|
|
|
|
|
|
|
const isModifierKey = event.ctrlKey || event.metaKey;
|
|
|
|
|
|
const key = event.key.toLowerCase();
|
|
|
|
|
|
if (isModifierKey && key === "z") {
|
2026-07-29 11:43:07 +08:00
|
|
|
|
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));
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-07-28 16:26:07 +08:00
|
|
|
|
const nodeErrorList = ref([]);
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
justLoadFlow();
|
|
|
|
|
|
initFlow();
|
|
|
|
|
|
document.addEventListener("singleNodeExecution", singleNodeExecution);
|
|
|
|
|
|
registerBeforeUnload();
|
2026-08-04 10:08:57 +08:00
|
|
|
|
window.addEventListener("keydown", handleKeyboardShortcut);
|
2026-08-11 16:50:30 +08:00
|
|
|
|
window.addEventListener("copy", copySelectedNodes);
|
|
|
|
|
|
window.addEventListener("paste", pasteCopiedNodes);
|
2026-07-28 16:26:07 +08:00
|
|
|
|
|
2026-07-29 16:17:39 +08:00
|
|
|
|
onFlowEvent("openParamsDrawer", (node) => {
|
2026-07-28 16:26:07 +08:00
|
|
|
|
showParamsDrawer.value = true;
|
|
|
|
|
|
paramsDrawerData.value = node;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-07-29 16:17:39 +08:00
|
|
|
|
onFlowEvent("throwAnError", (data) => {
|
2026-07-28 16:26:07 +08:00
|
|
|
|
// 处理错误抛出逻辑
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-07-29 16:17:39 +08:00
|
|
|
|
onFlowEvent("deleteNode", (nodeId) => {
|
2026-07-28 16:26:07 +08:00
|
|
|
|
const index = nodeErrorList.value.findIndex((item) => item === nodeId);
|
|
|
|
|
|
if (index !== -1) {
|
|
|
|
|
|
nodeErrorList.value.splice(index, 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
2026-07-29 16:17:39 +08:00
|
|
|
|
flowDisposed = true;
|
|
|
|
|
|
clearFlowPollTimer();
|
2026-07-29 11:43:07 +08:00
|
|
|
|
loopChildDragContexts.clear();
|
2026-07-29 16:17:39 +08:00
|
|
|
|
if (window.lf === lf) delete window.lf;
|
2026-07-28 16:26:07 +08:00
|
|
|
|
|
|
|
|
|
|
window.removeEventListener("beforeunload", handleBeforeUnload);
|
2026-08-04 10:08:57 +08:00
|
|
|
|
window.removeEventListener("keydown", handleKeyboardShortcut);
|
2026-08-11 16:50:30 +08:00
|
|
|
|
window.removeEventListener("copy", copySelectedNodes);
|
|
|
|
|
|
window.removeEventListener("paste", pasteCopiedNodes);
|
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
|
|
|
|
|
2026-08-05 09:27:55 +08:00
|
|
|
|
.single-node-param-row {
|
2026-08-05 09:53:23 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: nowrap;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
gap: 24px;
|
2026-08-05 09:27:55 +08:00
|
|
|
|
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) {
|
2026-08-05 09:53:23 +08:00
|
|
|
|
width: 220px;
|
2026-08-05 09:27:55 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.single-node-param-value :deep(.el-textarea__inner) {
|
|
|
|
|
|
min-height: 32px !important;
|
|
|
|
|
|
line-height: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-04 11:46:14 +08:00
|
|
|
|
.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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-05 09:27:55 +08:00
|
|
|
|
.device-lock-hint {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
margin-top: 6px;
|
|
|
|
|
|
color: var(--el-color-success);
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
line-height: 18px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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-29 16:17:39 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
2026-07-28 16:26:07 +08:00
|
|
|
|
|
|
|
|
|
|
.btn__container {
|
|
|
|
|
|
display: flex;
|
2026-07-29 16:17:39 +08:00
|
|
|
|
min-width: 0;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 10px;
|
2026-07-28 16:26:07 +08:00
|
|
|
|
|
|
|
|
|
|
.btn {
|
2026-07-29 16:17:39 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
|
|
|
|
|
|
.el-button + .el-button { margin-left: 0; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.publish-actions {
|
|
|
|
|
|
padding-left: 10px;
|
|
|
|
|
|
border-left: 1px solid #e4e8ef;
|
2026-07-28 16:26:07 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.container {
|
|
|
|
|
|
height: calc(100% - 60px);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.el-main {
|
|
|
|
|
|
padding: 0;
|
2026-07-29 16:17:39 +08:00
|
|
|
|
position: relative;
|
|
|
|
|
|
overflow: hidden;
|
2026-07-28 16:26:07 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.el-aside {
|
|
|
|
|
|
margin: 0;
|
2026-07-29 16:17:39 +08:00
|
|
|
|
width: 272px;
|
2026-07-28 16:26:07 +08:00
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-29 16:17:39 +08:00
|
|
|
|
.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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-28 16:26:07 +08:00
|
|
|
|
.flow__container {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
2026-08-05 09:53:23 +08:00
|
|
|
|
cursor: pointer;
|
2026-07-28 16:26:07 +08:00
|
|
|
|
|
2026-07-29 11:43:07 +08:00
|
|
|
|
:deep(.vue-flow__node) {
|
|
|
|
|
|
border: 0;
|
|
|
|
|
|
background: transparent;
|
2026-08-05 15:18:52 +08:00
|
|
|
|
cursor: grab;
|
2026-07-29 11:43:07 +08:00
|
|
|
|
}
|
2026-07-28 16:26:07 +08:00
|
|
|
|
|
2026-08-05 15:18:52 +08:00
|
|
|
|
:deep(.vue-flow__node.dragging) { cursor: grabbing; }
|
|
|
|
|
|
|
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) {
|
2026-08-05 09:53:23 +08:00
|
|
|
|
cursor: pointer;
|
2026-07-29 11:43:07 +08:00
|
|
|
|
}
|
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) {
|
2026-08-05 09:53:23 +08:00
|
|
|
|
cursor: pointer;
|
2026-07-29 11:43:07 +08:00
|
|
|
|
}
|
2026-07-28 16:26:07 +08:00
|
|
|
|
|
2026-07-29 11:43:07 +08:00
|
|
|
|
:deep(.vue-flow__pane.dragging) {
|
2026-08-05 15:18:52 +08:00
|
|
|
|
cursor: grabbing;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.vue-flow__selection) {
|
|
|
|
|
|
border: 1px solid #2f80ed;
|
|
|
|
|
|
background: rgba(47, 128, 237, 0.1);
|
|
|
|
|
|
box-shadow: 0 0 0 1px rgba(47, 128, 237, 0.08);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.vue-flow__nodesselection-rect) {
|
|
|
|
|
|
border: 1px dashed rgba(47, 128, 237, 0.72);
|
|
|
|
|
|
background: rgba(47, 128, 237, 0.04);
|
2026-07-29 11:43:07 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
: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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
</style>
|