From 3d343812a94b05120dca41fa9785b7a222b2c3f3 Mon Sep 17 00:00:00 2001
From: lixiaolong <702156524@qq.com>
Date: Thu, 13 Aug 2026 17:16:42 +0800
Subject: [PATCH] =?UTF-8?q?feat(flow):=20=E6=B7=BB=E5=8A=A0=E5=8A=A8?=
=?UTF-8?q?=E4=BD=9C=E9=98=9F=E5=88=97=E6=AD=A5=E9=AA=A4=E7=AE=A1=E7=90=86?=
=?UTF-8?q?=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 新增 actionQueue.js 工具函数,包含步骤移动、复制、名称获取等功能
- 在 ActionQueueParams.vue 中实现步骤名称编辑和复制功能
- 添加步骤上下移动和删除操作的工具提示
- 实现步骤名称显示和索引编号的样式优化
- 在 ActionQueueSingleRun.vue 中添加步骤复制和移动功能
- 重构步骤数据结构,添加运行时键值和名称字段
- 更新样式以支持新的步骤头部布局和操作按钮
---
src/utils/actionQueue.js | 32 +++++++++
.../flow/components/ActionQueueSingleRun.vue | 72 +++++++++++++++++--
.../components/params/ActionQueueParams.vue | 45 +++++++++---
3 files changed, 136 insertions(+), 13 deletions(-)
create mode 100644 src/utils/actionQueue.js
diff --git a/src/utils/actionQueue.js b/src/utils/actionQueue.js
new file mode 100644
index 0000000..5911bfa
--- /dev/null
+++ b/src/utils/actionQueue.js
@@ -0,0 +1,32 @@
+const deepClone = (value) => JSON.parse(JSON.stringify(value))
+
+export const getActionQueueStepName = (step, index) => {
+ const name = String(step?.stepName || '').trim()
+ return name || `步骤 ${index + 1}`
+}
+
+export const moveActionQueueStep = (steps, index, offset) => {
+ const targetIndex = index + offset
+ if (!Array.isArray(steps) || index < 0 || targetIndex < 0 || targetIndex >= steps.length) return false
+ const [step] = steps.splice(index, 1)
+ steps.splice(targetIndex, 0, step)
+ return true
+}
+
+export const copyActionQueueStep = (step, overrides = {}) => {
+ const copied = deepClone(step)
+ const originalName = String(step?.stepName || '').trim()
+ return {
+ ...copied,
+ ...overrides,
+ stepName: `${originalName || '未命名步骤'} - 副本`,
+ }
+}
+
+export const toActionQueueExecutionSteps = (steps) => (Array.isArray(steps) ? steps : []).map((step, index) => ({
+ stepId: `step_${index + 1}`,
+ type: step.type,
+ ...(step.deviceId ? { deviceId: step.deviceId } : {}),
+ timeoutMs: Number(step.timeoutMs || 0),
+ params: deepClone(step.params || {}),
+}))
diff --git a/src/views/flow/components/ActionQueueSingleRun.vue b/src/views/flow/components/ActionQueueSingleRun.vue
index f2f9932..3a6d054 100644
--- a/src/views/flow/components/ActionQueueSingleRun.vue
+++ b/src/views/flow/components/ActionQueueSingleRun.vue
@@ -11,8 +11,24 @@
-
-
步骤 {{ index + 1 }} · {{ actionName(step.type) }}
+
+
+
+
+
import { ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
+import { ArrowDown, ArrowUp, CopyDocument } from '@element-plus/icons-vue'
import { getRobotDevicesByRobotId } from '@/api/inspection/robot'
import { DEVICE_KIND } from '@/utils/robotDevice'
+import { copyActionQueueStep, getActionQueueStepName, moveActionQueueStep, toActionQueueExecutionSteps } from '@/utils/actionQueue'
/**
* 组件输入配置,仅在当前单次动作队列运行表单内使用。
@@ -58,6 +76,8 @@ const steps = ref([])
const devices = ref([])
/** 设备接口请求状态,仅控制当前组件设备选择框的加载效果。 */
const deviceLoading = ref(false)
+let runtimeKeySequence = 0
+const createRuntimeKey = () => `queue_runtime_${Date.now()}_${runtimeKeySequence += 1}`
/**
* 获取动作类型的展示名称。
@@ -96,11 +116,26 @@ const initData = async () => {
const values = Object.fromEntries(props.nodeParams.map((item) => [item.name, item.input]))
actionId.value = values.actionId || ''
totalTimeoutMs.value = Number(values.totalTimeoutMs || 60000)
- steps.value = JSON.parse(JSON.stringify(Array.isArray(values.steps) ? values.steps : []))
+ steps.value = JSON.parse(JSON.stringify(Array.isArray(values.steps) ? values.steps : [])).map((step, index) => ({
+ ...step,
+ stepName: getActionQueueStepName(step, index),
+ runtimeKey: createRuntimeKey(),
+ }))
robotId.value = props.initialRobotId || ''
if (robotId.value) await loadDevices(robotId.value)
}
+const moveStep = (index, offset) => {
+ moveActionQueueStep(steps.value, index, offset)
+}
+
+const copyStep = (index) => {
+ const source = steps.value[index]
+ if (!source) return
+ const copied = copyActionQueueStep(source, { runtimeKey: createRuntimeKey() })
+ steps.value.splice(index + 1, 0, copied)
+}
+
/**
* 加载指定机器人的设备并重建各步骤设备选择,影响当前组件的 devices 和 steps。
* 切换机器人后旧 deviceId 已不再可靠,因此先清空设备及所有步骤选择;无机器人时直接结束。
@@ -144,7 +179,12 @@ const validate = () => {
}
/** 汇总当前表单数据为运行接口载荷;返回范围包含机器人、队列配置和全部步骤。 */
-const getPayload = () => ({ robotId: robotId.value, actionId: actionId.value, totalTimeoutMs: totalTimeoutMs.value, steps: steps.value })
+const getPayload = () => ({
+ robotId: robotId.value,
+ actionId: actionId.value,
+ totalTimeoutMs: totalTimeoutMs.value,
+ steps: toActionQueueExecutionSteps(steps.value),
+})
/** 返回当前选中的机器人 ID,供父组件处理兼容字段或运行上下文。 */
const getRobotId = () => robotId.value
@@ -164,11 +204,33 @@ defineExpose({ validate, getPayload, getRobotId })
border-radius: 8px;
}
-.single-queue-step__title {
+.single-queue-step__header {
+ display: flex;
+ min-width: 0;
margin-bottom: 10px;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+}
+
+.single-queue-step__title {
+ overflow: hidden;
color: #1f2937;
font-size: 13px;
font-weight: 650;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.single-queue-step__actions {
+ display: flex;
+ flex: 0 0 auto;
+ align-items: center;
+ gap: 6px;
+}
+
+.single-queue-step__actions :deep(.el-button + .el-button) {
+ margin-left: 0;
}
.single-queue-step__params {
diff --git a/src/views/flow/components/params/ActionQueueParams.vue b/src/views/flow/components/params/ActionQueueParams.vue
index dcca245..8dc5c2e 100644
--- a/src/views/flow/components/params/ActionQueueParams.vue
+++ b/src/views/flow/components/params/ActionQueueParams.vue
@@ -28,18 +28,31 @@
+
+
+
@@ -118,7 +131,7 @@