CMVR-IOT-UI/src/views/flow/components/ActionQueueSingleRun.vue
lixiaolong 6be8bc9bfd feat(flow): 添加动作队列步骤独立执行功能和媒体分析增强
- 在动作队列参数组件中添加单独执行按钮,允许独立运行指定步骤
- 实现步骤验证和构建逻辑,支持单步执行的参数处理
- 更新媒体分析参数界面,添加高级设置和参数微调功能
- 增加视频分析的置信度、通过标准等配置选项
- 优化任务执行日志显示,添加分析结果的结构化展示
- 集成分页请求工具,改进日志加载性能和用户体验
- 添加执行步骤索引跟踪,确保执行状态正确管理
- 实现防重复ID机制,保证分页数据完整性
- 新增分析结果解析和可视化展示功能
- 扩展媒体分析类型的处理和结果显示
2026-08-17 16:05:42 +08:00

282 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<el-form label-position="top">
<el-form-item label="机器人" required>
<el-select v-model="robotId" filterable style="width: 100%" @change="loadDevices">
<el-option v-for="item in robotOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="队列总超时(毫秒)" required>
<el-input-number v-model="totalTimeoutMs" :min="1" :max="2147483647" style="width: 100%" />
</el-form-item>
<el-form-item label="动作标识(可选)">
<el-input v-model="actionId" placeholder="留空时自动生成" />
</el-form-item>
<div v-for="(step, index) in steps" :key="step.runtimeKey" class="single-queue-step">
<div class="single-queue-step__header">
<div class="single-queue-step__title">{{ getActionQueueStepName(step, index) }} · {{ actionName(step.type) }}</div>
<div class="single-queue-step__actions">
<el-tooltip content="单独执行此步骤" placement="top">
<el-button
:icon="VideoPlay"
circle
size="small"
type="primary"
plain
aria-label="单独执行此步骤"
:loading="executingStepIndex === index"
:disabled="executingStepIndex >= 0 && executingStepIndex !== index"
@click="emit('execute-step', index)"
/>
</el-tooltip>
<el-tooltip content="上移一步" placement="top">
<el-button :icon="ArrowUp" circle size="small" :disabled="index === 0" aria-label="上移一步" @click="moveStep(index, -1)" />
</el-tooltip>
<el-tooltip content="下移一步" placement="top">
<el-button :icon="ArrowDown" circle size="small" :disabled="index === steps.length - 1" aria-label="下移一步" @click="moveStep(index, 1)" />
</el-tooltip>
<el-tooltip content="复制步骤" placement="top">
<el-button :icon="CopyDocument" circle size="small" aria-label="复制步骤" @click="copyStep(index)" />
</el-tooltip>
</div>
</div>
<el-form-item label="步骤名称">
<el-input v-model="step.stepName" maxlength="50" show-word-limit placeholder="例如:移动到取料位" />
</el-form-item>
<el-form-item v-if="deviceKind(step.type)" label="执行设备" required>
<el-select v-model="step.deviceId" filterable :loading="deviceLoading" style="width: 100%">
<el-option v-for="device in devicesForStep(step)" :key="device.deviceId"
:label="device.deviceName && device.deviceName !== device.deviceId ? `${device.deviceName} (${device.deviceId})` : device.deviceId"
:value="device.deviceId" />
</el-select>
</el-form-item>
<div class="single-queue-step__params">{{ stepSummary(step) }}</div>
</div>
</el-form>
</template>
<script setup>
import { ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { ArrowDown, ArrowUp, CopyDocument, VideoPlay } 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'
/**
* 组件输入配置,仅在当前单次动作队列运行表单内使用。
* nodeParams 提供流程节点参数,robotOptions 提供可选机器人,initialRobotId 用于初始化默认机器人。
*/
const props = defineProps({
nodeParams: { type: Array, default: () => [] },
robotOptions: { type: Array, default: () => [] },
initialRobotId: { type: String, default: '' },
executingStepIndex: { type: Number, default: -1 },
})
const emit = defineEmits(['execute-step'])
/** 动作类型到中文名称的映射,作用范围为当前组件的步骤标题展示。 */
const labels = {
DELAY: '等待', ARM_MOVE_J: '机械臂关节运动', ARM_MOVE_L: '机械臂直线运动',
AGV_NAVIGATE_TO_POSE: '底盘导航到坐标', AGV_NAVIGATE_TO_STATION: '底盘导航到站点', AGV_FOLLOW_PATH: '底盘按路径导航',
}
/** 当前选中的机器人 ID,作用于设备查询、校验和最终运行请求。 */
const robotId = ref('')
/** 本次动作队列标识;为空时由服务端自动生成,仅随当前运行请求提交。 */
const actionId = ref('')
/** 整个动作队列的超时时间(毫秒),作用于当前运行请求。 */
const totalTimeoutMs = ref(60000)
/** 当前节点解析出的动作步骤副本,组件内可补充 deviceId,不回写原始 props。 */
const steps = ref([])
/** 当前机器人挂载的设备列表,用于为各动作步骤筛选可执行设备。 */
const devices = ref([])
/** 设备接口请求状态,仅控制当前组件设备选择框的加载效果。 */
const deviceLoading = ref(false)
let runtimeKeySequence = 0
const createRuntimeKey = () => `queue_runtime_${Date.now()}_${runtimeKeySequence += 1}`
/**
* 获取动作类型的展示名称。
* 先查询本地中文映射,未配置的类型直接显示原值,避免未知动作显示为空。
*/
const actionName = (type) => labels[type] || type
/**
* 根据动作名前缀判断所需设备类型。
* 当前组件只处理机械臂和底盘队列动作;等待等无设备动作返回 null。
*/
const deviceKind = (type) => type?.startsWith('ARM_') ? DEVICE_KIND.ARM : (type?.startsWith('AGV_') ? DEVICE_KIND.AGV : null)
/**
* 筛选单个步骤可使用的设备,范围限定为当前所选机器人的在线设备结果。
* 将接口 deviceKind 与动作要求统一转为数值后比较,兼容字符串和数字类型。
*/
const devicesForStep = (step) => devices.value.filter((device) => Number(device.deviceKind) === deviceKind(step.type))
/**
* 生成步骤参数摘要,仅用于当前组件的只读预览。
* 等待和站点导航使用易读文案,其余动作保留完整 JSON 信息。
*/
const stepSummary = (step) => {
if (step.type === 'DELAY') return `等待 ${step.params?.durationMs || 0} 毫秒`
if (step.type === 'AGV_NAVIGATE_TO_STATION') return `站点 ${step.params?.stationId || '-'}`
return JSON.stringify(step.params || {})
}
/**
* 使用节点参数重新初始化表单,作用于首次渲染及输入属性变化。
* 步骤:1. 将参数数组转成按名称索引的对象;2. 设置基本字段并深拷贝步骤;
* 3. 应用默认机器人;4. 若存在机器人,则加载设备并为步骤建立默认设备选择。
*/
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 : [])).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 已不再可靠,因此先清空设备及所有步骤选择;无机器人时直接结束。
* 接口返回后逐步按动作类型筛选设备,并默认选择第一个匹配项;finally 保证加载状态复位。
*/
const loadDevices = async (value) => {
// 机器人发生变化时,旧设备可能属于另一台机器人,必须整体清除以避免误提交。
devices.value = []
steps.value.forEach((step) => { step.deviceId = '' })
if (!value) return
deviceLoading.value = true
try {
const response = await getRobotDevicesByRobotId(value)
devices.value = Array.isArray(response.data) ? response.data : []
// 每个步骤独立按设备类型匹配;有候选项时预选首项,减少单一设备场景下的重复操作。
steps.value.forEach((step) => {
const options = devicesForStep(step)
if (options.length) step.deviceId = options[0].deviceId
})
} finally {
deviceLoading.value = false
}
}
/**
* 校验当前动作队列是否具备运行条件,供父组件在提交前调用。
* 依次检查机器人、队列非空及每个需要设备的步骤;首次失败即提示并终止,全部通过返回 true。
*/
const validate = () => {
if (!robotId.value) return ElMessage.warning('请选择机器人'), false
if (!steps.value.length) return ElMessage.warning('动作队列不能为空'), false
// 等待类步骤无需设备;机械臂或底盘步骤必须存在与当前机器人匹配的 deviceId。
for (let index = 0; index < steps.value.length; index += 1) {
const step = steps.value[index]
if (deviceKind(step.type) && !step.deviceId) {
ElMessage.warning(`步骤 ${index + 1} 没有可用的执行设备`)
return false
}
}
return true
}
const validateStep = (index) => {
if (!robotId.value) return ElMessage.warning('请选择机器人'), false
const step = steps.value[index]
if (!step) return ElMessage.warning('未找到要执行的步骤'), false
if (deviceKind(step.type) && !step.deviceId) {
ElMessage.warning(`步骤 ${index + 1} 没有可用的执行设备`)
return false
}
return true
}
/** 汇总当前表单数据为运行接口载荷;返回范围包含机器人、队列配置和全部步骤。 */
const getPayload = () => ({
robotId: robotId.value,
actionId: actionId.value,
totalTimeoutMs: totalTimeoutMs.value,
steps: toActionQueueExecutionSteps(steps.value),
})
const getStepPayload = (index) => {
const step = steps.value[index]
const stepTimeoutMs = Math.max(0, Number(step?.timeoutMs || 0))
return {
robotId: robotId.value,
actionId: '',
totalTimeoutMs: Math.max(1000, stepTimeoutMs + 5000),
steps: toActionQueueExecutionSteps(step ? [step] : []),
}
}
/** 返回当前选中的机器人 ID,供父组件处理兼容字段或运行上下文。 */
const getRobotId = () => robotId.value
/** 监听节点配置和默认机器人变化,深度变化时重建当前组件的表单及设备选项。 */
watch(() => [props.nodeParams, props.initialRobotId], initData, { immediate: true, deep: true })
/** 向父组件开放提交前校验、载荷读取和机器人 ID 读取能力。 */
defineExpose({ validate, validateStep, getPayload, getStepPayload, getRobotId })
</script>
<style scoped>
.single-queue-step {
margin-bottom: 10px;
padding: 12px;
border: 1px solid #dfe4ec;
border-radius: 8px;
}
.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 {
overflow: hidden;
color: #7a8494;
font-size: 12px;
line-height: 18px;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>