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 @@