feat(flow): 添加动作队列节点功能
- 新增 actionQueue 节点类型,支持在边缘端按顺序连续执行多个动作 - 实现 ActionQueueParams 组件,提供执行设置和动作步骤配置界面 - 添加 ActionQueueSingleRun 组件,支持动作队列的单节点执行 - 更新 flowResources 工具函数,完善设备绑定和资源管理逻辑 - 集成动作队列到流程编辑器,支持参数配置和设备分配 - 优化节点渲染显示,支持多设备用途的角色展示
This commit is contained in:
parent
c99cd51d34
commit
16481996b9
@ -62,6 +62,7 @@ export const getResourceRoles = (graph = globalThis.lf?.getGraphData?.()) => (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const isRobotNode = (node) => node?.properties?.nodeType === 'EDGE'
|
const isRobotNode = (node) => node?.properties?.nodeType === 'EDGE'
|
||||||
|
export const ACTION_QUEUE_ACTION = 'SYSTEM_EXECUTE_ACTION_QUEUE'
|
||||||
export const actionRequiresDevice = (action) => (
|
export const actionRequiresDevice = (action) => (
|
||||||
getDeviceKindForAction(action) != null || action === 'DEVICE_EXECUTE_JSON_COMMAND'
|
getDeviceKindForAction(action) != null || action === 'DEVICE_EXECUTE_JSON_COMMAND'
|
||||||
)
|
)
|
||||||
@ -139,7 +140,9 @@ export const normalizeFlowResourceSchema = (sourceGraph) => {
|
|||||||
}
|
}
|
||||||
target.roleKey ||= defaultRole.roleKey
|
target.roleKey ||= defaultRole.roleKey
|
||||||
target.dispatchPolicy ||= 'ONE'
|
target.dispatchPolicy ||= 'ONE'
|
||||||
target.deviceSlotKey ||= target.deviceBindings?.[0]?.slotKey || ''
|
const isActionQueue = properties.action === ACTION_QUEUE_ACTION
|
||||||
|
if (!isActionQueue) target.deviceSlotKey ||= target.deviceBindings?.[0]?.slotKey || ''
|
||||||
|
target.deviceBindings = Array.isArray(target.deviceBindings) ? target.deviceBindings : []
|
||||||
|
|
||||||
const targetRole = roles.find((role) => role.roleKey === target.roleKey) || defaultRole
|
const targetRole = roles.find((role) => role.roleKey === target.roleKey) || defaultRole
|
||||||
const deviceParam = properties.nodeParams?.find((param) => param.name === 'deviceId')
|
const deviceParam = properties.nodeParams?.find((param) => param.name === 'deviceId')
|
||||||
@ -147,11 +150,15 @@ export const normalizeFlowResourceSchema = (sourceGraph) => {
|
|||||||
const slot = findOrCreateLegacySlot(targetRole, node, legacySlotByIdentity, deviceParam?.input)
|
const slot = findOrCreateLegacySlot(targetRole, node, legacySlotByIdentity, deviceParam?.input)
|
||||||
if (slot) target.deviceSlotKey = slot.slotKey
|
if (slot) target.deviceSlotKey = slot.slotKey
|
||||||
}
|
}
|
||||||
const referencedSlot = targetRole.deviceSlots.find((slot) => slot.slotKey === target.deviceSlotKey)
|
const referencedSlotKeys = new Set([
|
||||||
if (referencedSlot && !referencedSlot.managedBy) {
|
target.deviceSlotKey,
|
||||||
referencedSlot.managedBy = FLOW_NODE_RESOURCE_OWNER
|
...target.deviceBindings.map((binding) => binding?.slotKey),
|
||||||
}
|
].filter(Boolean))
|
||||||
delete target.deviceBindings
|
targetRole.deviceSlots.forEach((slot) => {
|
||||||
|
if (referencedSlotKeys.has(slot.slotKey) && !slot.managedBy) slot.managedBy = FLOW_NODE_RESOURCE_OWNER
|
||||||
|
})
|
||||||
|
if (!isActionQueue) delete target.deviceBindings
|
||||||
|
if (isActionQueue) delete target.deviceSlotKey
|
||||||
properties.nodeParams = (properties.nodeParams || []).filter((param) => param.name !== 'deviceId')
|
properties.nodeParams = (properties.nodeParams || []).filter((param) => param.name !== 'deviceId')
|
||||||
properties.executionTarget = target
|
properties.executionTarget = target
|
||||||
})
|
})
|
||||||
@ -182,9 +189,11 @@ export const pruneUnusedNodeResources = (sourceGraph) => {
|
|||||||
const target = node.properties?.executionTarget
|
const target = node.properties?.executionTarget
|
||||||
if (!target?.roleKey) return
|
if (!target?.roleKey) return
|
||||||
usedRoleKeys.add(target.roleKey)
|
usedRoleKeys.add(target.roleKey)
|
||||||
if (!target.deviceSlotKey) return
|
|
||||||
if (!usedSlotsByRole.has(target.roleKey)) usedSlotsByRole.set(target.roleKey, new Set())
|
if (!usedSlotsByRole.has(target.roleKey)) usedSlotsByRole.set(target.roleKey, new Set())
|
||||||
usedSlotsByRole.get(target.roleKey).add(target.deviceSlotKey)
|
if (target.deviceSlotKey) usedSlotsByRole.get(target.roleKey).add(target.deviceSlotKey)
|
||||||
|
;(target.deviceBindings || []).forEach((binding) => {
|
||||||
|
if (binding?.slotKey) usedSlotsByRole.get(target.roleKey).add(binding.slotKey)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
let removedSlots = 0
|
let removedSlots = 0
|
||||||
@ -240,6 +249,13 @@ export const validateFlowResources = (sourceGraph) => {
|
|||||||
if (target.deviceSlotKey && !(role.deviceSlots || []).some((slot) => slot.slotKey === target.deviceSlotKey)) {
|
if (target.deviceSlotKey && !(role.deviceSlots || []).some((slot) => slot.slotKey === target.deviceSlotKey)) {
|
||||||
errors.push({ nodeId: node.id, message: `${node.properties?.name || node.id}引用的设备用途不存在` })
|
errors.push({ nodeId: node.id, message: `${node.properties?.name || node.id}引用的设备用途不存在` })
|
||||||
}
|
}
|
||||||
|
;(target.deviceBindings || []).forEach((binding) => {
|
||||||
|
if (!binding?.parameterName || !binding?.slotKey) {
|
||||||
|
errors.push({ nodeId: node.id, message: `${node.properties?.name || node.id}存在未配置的队列设备` })
|
||||||
|
} else if (!(role.deviceSlots || []).some((slot) => slot.slotKey === binding.slotKey)) {
|
||||||
|
errors.push({ nodeId: node.id, message: `${node.properties?.name || node.id}引用的队列设备用途不存在` })
|
||||||
|
}
|
||||||
|
})
|
||||||
if (nodeRequiresDevice(node)
|
if (nodeRequiresDevice(node)
|
||||||
&& !target.deviceSlotKey) {
|
&& !target.deviceSlotKey) {
|
||||||
errors.push({ nodeId: node.id, message: `${node.properties?.name || node.id}未选择设备用途` })
|
errors.push({ nodeId: node.id, message: `${node.properties?.name || node.id}未选择设备用途` })
|
||||||
@ -290,21 +306,34 @@ export const mergeCopiedResourceRoles = (targetGraph, sourceRoles = [], copiedNo
|
|||||||
targetRole ||= targetRoles[0]
|
targetRole ||= targetRoles[0]
|
||||||
target.roleKey = targetRole.roleKey
|
target.roleKey = targetRole.roleKey
|
||||||
|
|
||||||
if (target.deviceSlotKey && !targetRole.deviceSlots?.some((slot) => slot.slotKey === target.deviceSlotKey)) {
|
const remapSlot = (slotKey) => {
|
||||||
const sourceSlot = sourceRole?.deviceSlots?.find((slot) => slot.slotKey === target.deviceSlotKey)
|
if (!slotKey || targetRole.deviceSlots?.some((slot) => slot.slotKey === slotKey)) return slotKey
|
||||||
|
const sourceSlot = sourceRole?.deviceSlots?.find((slot) => slot.slotKey === slotKey)
|
||||||
const sameSlot = targetRole.deviceSlots?.find((slot) => (
|
const sameSlot = targetRole.deviceSlots?.find((slot) => (
|
||||||
sourceSlot && slot.displayName === sourceSlot.displayName && Number(slot.deviceKind) === Number(sourceSlot.deviceKind)
|
sourceSlot && slot.displayName === sourceSlot.displayName && Number(slot.deviceKind) === Number(sourceSlot.deviceKind)
|
||||||
))
|
))
|
||||||
if (sameSlot) {
|
if (sameSlot) return sameSlot.slotKey
|
||||||
target.deviceSlotKey = sameSlot.slotKey
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const newSlot = sourceSlot ? clone(sourceSlot) : createDeviceSlot(getDeviceKindForAction(node.properties?.action), targetRole.deviceSlots)
|
const newSlot = sourceSlot ? clone(sourceSlot) : createDeviceSlot(getDeviceKindForAction(node.properties?.action), targetRole.deviceSlots)
|
||||||
newSlot.managedBy = FLOW_NODE_RESOURCE_OWNER
|
newSlot.managedBy = FLOW_NODE_RESOURCE_OWNER
|
||||||
delete newSlot.legacyDeviceId
|
delete newSlot.legacyDeviceId
|
||||||
targetRole.deviceSlots.push(newSlot)
|
targetRole.deviceSlots.push(newSlot)
|
||||||
target.deviceSlotKey = newSlot.slotKey
|
|
||||||
addedSlots += 1
|
addedSlots += 1
|
||||||
|
return newSlot.slotKey
|
||||||
|
}
|
||||||
|
target.deviceSlotKey = remapSlot(target.deviceSlotKey)
|
||||||
|
;(target.deviceBindings || []).forEach((binding) => {
|
||||||
|
binding.slotKey = remapSlot(binding.slotKey)
|
||||||
|
})
|
||||||
|
const bindingSlotByName = new Map((target.deviceBindings || []).map((binding) => (
|
||||||
|
[binding.parameterName, binding.slotKey]
|
||||||
|
)))
|
||||||
|
const queueStepsParam = node.properties?.nodeParams?.find((param) => param.name === 'steps')
|
||||||
|
if (Array.isArray(queueStepsParam?.input)) {
|
||||||
|
queueStepsParam.input.forEach((step) => {
|
||||||
|
if (bindingSlotByName.has(step?.deviceBindingKey)) {
|
||||||
|
step.deviceSlotKey = bindingSlotByName.get(step.deviceBindingKey)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
111
src/views/flow/components/ActionQueueSingleRun.vue
Normal file
111
src/views/flow/components/ActionQueueSingleRun.vue
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
<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.stepId || index" class="single-queue-step">
|
||||||
|
<div class="single-queue-step__title">步骤 {{ index + 1 }} · {{ actionName(step.type) }}</div>
|
||||||
|
<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 { getRobotDevicesByRobotId } from '@/api/inspection/robot'
|
||||||
|
import { DEVICE_KIND } from '@/utils/robotDevice'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
nodeParams: { type: Array, default: () => [] },
|
||||||
|
robotOptions: { type: Array, default: () => [] },
|
||||||
|
initialRobotId: { type: String, default: '' },
|
||||||
|
})
|
||||||
|
|
||||||
|
const labels = {
|
||||||
|
DELAY: '等待', ARM_MOVE_J: '机械臂关节运动', ARM_MOVE_L: '机械臂直线运动',
|
||||||
|
AGV_NAVIGATE_TO_POSE: '底盘导航到坐标', AGV_NAVIGATE_TO_STATION: '底盘导航到站点', AGV_FOLLOW_PATH: '底盘按路径导航',
|
||||||
|
}
|
||||||
|
const robotId = ref('')
|
||||||
|
const actionId = ref('')
|
||||||
|
const totalTimeoutMs = ref(60000)
|
||||||
|
const steps = ref([])
|
||||||
|
const devices = ref([])
|
||||||
|
const deviceLoading = ref(false)
|
||||||
|
const actionName = (type) => labels[type] || type
|
||||||
|
const deviceKind = (type) => type?.startsWith('ARM_') ? DEVICE_KIND.ARM : (type?.startsWith('AGV_') ? DEVICE_KIND.AGV : null)
|
||||||
|
const devicesForStep = (step) => devices.value.filter((device) => Number(device.deviceKind) === deviceKind(step.type))
|
||||||
|
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 || {})
|
||||||
|
}
|
||||||
|
|
||||||
|
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 : []))
|
||||||
|
robotId.value = props.initialRobotId || ''
|
||||||
|
if (robotId.value) await loadDevices(robotId.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const validate = () => {
|
||||||
|
if (!robotId.value) return ElMessage.warning('请选择机器人'), false
|
||||||
|
if (!steps.value.length) return ElMessage.warning('动作队列不能为空'), false
|
||||||
|
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 getPayload = () => ({ robotId: robotId.value, actionId: actionId.value, totalTimeoutMs: totalTimeoutMs.value, steps: steps.value })
|
||||||
|
const getRobotId = () => robotId.value
|
||||||
|
|
||||||
|
watch(() => [props.nodeParams, props.initialRobotId], initData, { immediate: true, deep: true })
|
||||||
|
defineExpose({ validate, getPayload, getRobotId })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.single-queue-step { margin-bottom: 10px; padding: 12px; border: 1px solid #dfe4ec; border-radius: 8px; }
|
||||||
|
.single-queue-step__title { margin-bottom: 10px; color: #1f2937; font-size: 13px; font-weight: 650; }
|
||||||
|
.single-queue-step__params { overflow: hidden; color: #7a8494; font-size: 12px; line-height: 18px; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
</style>
|
||||||
@ -37,6 +37,7 @@ import SdAgentNodeParams from './params/SdAgentNodeParams.vue'
|
|||||||
import RecognizeNodeParams from './params/RecognizeNodeParams.vue'
|
import RecognizeNodeParams from './params/RecognizeNodeParams.vue'
|
||||||
import DeviceUniversal from './params/DeviceUniversal.vue'
|
import DeviceUniversal from './params/DeviceUniversal.vue'
|
||||||
import SwitchNodeParams from './params/SwitchNodeParams.vue'
|
import SwitchNodeParams from './params/SwitchNodeParams.vue'
|
||||||
|
import ActionQueueParams from './params/ActionQueueParams.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
drawer: Boolean,
|
drawer: Boolean,
|
||||||
@ -63,6 +64,7 @@ const currentComponent = computed(() => {
|
|||||||
if (type === 'sdAgent') return SdAgentNodeParams
|
if (type === 'sdAgent') return SdAgentNodeParams
|
||||||
if (type === 'recognize') return RecognizeNodeParams
|
if (type === 'recognize') return RecognizeNodeParams
|
||||||
if (['deviceUniversal', 'device_universal'].includes(type)) return DeviceUniversal
|
if (['deviceUniversal', 'device_universal'].includes(type)) return DeviceUniversal
|
||||||
|
if (type === 'actionQueue') return ActionQueueParams
|
||||||
if (type === 'branch') return SwitchNodeParams
|
if (type === 'branch') return SwitchNodeParams
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|||||||
293
src/views/flow/components/params/ActionQueueParams.vue
Normal file
293
src/views/flow/components/params/ActionQueueParams.vue
Normal file
@ -0,0 +1,293 @@
|
|||||||
|
<template>
|
||||||
|
<div class="queue-editor">
|
||||||
|
<div class="queue-editor__heading">执行设置</div>
|
||||||
|
<el-form label-position="top">
|
||||||
|
<el-form-item label="由谁执行" required>
|
||||||
|
<el-select v-model="formData.executionTarget.roleKey" style="width: 100%" @change="handleRoleChange">
|
||||||
|
<el-option v-for="role in roles" :key="role.roleKey" :label="role.displayName" :value="role.roleKey" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="队列总超时(毫秒)" required>
|
||||||
|
<el-input-number v-model="formData.totalTimeoutMs" :min="1" :max="2147483647" style="width: 100%" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="动作标识(可选)">
|
||||||
|
<el-input v-model="formData.actionId" placeholder="留空时执行端自动生成" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<div class="queue-editor__toolbar">
|
||||||
|
<div>
|
||||||
|
<div class="queue-editor__heading">动作步骤</div>
|
||||||
|
<div class="queue-editor__hint">步骤会在边缘端严格按顺序执行。</div>
|
||||||
|
</div>
|
||||||
|
<el-button type="primary" plain @click="addStep"><el-icon><Plus /></el-icon>添加步骤</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-for="(step, index) in formData.steps" :key="step.localKey" class="queue-step">
|
||||||
|
<div class="queue-step__header">
|
||||||
|
<span>步骤 {{ index + 1 }}</span>
|
||||||
|
<div>
|
||||||
|
<el-button :icon="ArrowUp" circle size="small" :disabled="index === 0" title="上移" @click="moveStep(index, -1)" />
|
||||||
|
<el-button :icon="ArrowDown" circle size="small" :disabled="index === formData.steps.length - 1" title="下移" @click="moveStep(index, 1)" />
|
||||||
|
<el-button :icon="Delete" circle size="small" type="danger" plain title="删除" @click="removeStep(index)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-form label-position="top">
|
||||||
|
<el-form-item label="动作" required>
|
||||||
|
<el-select v-model="step.type" style="width: 100%" @change="handleStepTypeChange(step)">
|
||||||
|
<el-option v-for="item in actionTypes" :key="item.value" :label="item.label" :value="item.value" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="deviceKindForType(step.type)" label="使用设备" required>
|
||||||
|
<el-select v-model="step.deviceSlotKey" style="width: 100%">
|
||||||
|
<el-option
|
||||||
|
v-for="slot in matchingSlots(step.type)"
|
||||||
|
:key="slot.slotKey"
|
||||||
|
:label="slot.displayName"
|
||||||
|
:value="slot.slotKey"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
<div v-if="!matchingSlots(step.type).length" class="queue-editor__warning">当前角色没有对应设备用途。</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="步骤超时(毫秒)">
|
||||||
|
<el-input-number v-model="step.timeoutMs" :min="0" :max="2147483647" style="width: 100%" />
|
||||||
|
</el-form-item>
|
||||||
|
<template v-for="field in fieldsForType(step.type)" :key="field.key">
|
||||||
|
<el-form-item :label="field.label" :required="field.required !== false">
|
||||||
|
<el-select v-if="field.options" v-model="step.params[field.key]" style="width: 100%">
|
||||||
|
<el-option v-for="option in field.options" :key="option.value" :label="option.label" :value="option.value" />
|
||||||
|
</el-select>
|
||||||
|
<el-input-number
|
||||||
|
v-else-if="field.number"
|
||||||
|
v-model="step.params[field.key]"
|
||||||
|
:min="field.min"
|
||||||
|
:max="field.max ?? Infinity"
|
||||||
|
:step="field.step || 1"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
<el-input v-else v-model="step.params[field.key]" :type="field.multiline ? 'textarea' : 'text'" :rows="3" />
|
||||||
|
</el-form-item>
|
||||||
|
</template>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
<el-empty v-if="!formData.steps.length" description="请添加至少一个动作步骤" :image-size="72" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { reactive, ref, watch } from 'vue'
|
||||||
|
import { ArrowDown, ArrowUp, Delete, Plus } from '@element-plus/icons-vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import {
|
||||||
|
createDeviceSlot,
|
||||||
|
createResourceKey,
|
||||||
|
FLOW_NODE_RESOURCE_OWNER,
|
||||||
|
getResourceRoles,
|
||||||
|
getStartNode,
|
||||||
|
pruneUnusedNodeResources,
|
||||||
|
} from '@/utils/flowResources'
|
||||||
|
import { DEVICE_KIND } from '@/utils/robotDevice'
|
||||||
|
|
||||||
|
const props = defineProps({ data: { type: Object, required: true } })
|
||||||
|
const emit = defineEmits(['save-success', 'save-error'])
|
||||||
|
|
||||||
|
const actionTypes = [
|
||||||
|
{ value: 'DELAY', label: '等待' },
|
||||||
|
{ value: 'ARM_MOVE_J', label: '机械臂关节运动' },
|
||||||
|
{ value: 'ARM_MOVE_L', label: '机械臂直线运动' },
|
||||||
|
{ value: 'AGV_NAVIGATE_TO_POSE', label: '底盘导航到坐标' },
|
||||||
|
{ value: 'AGV_NAVIGATE_TO_STATION', label: '底盘导航到站点' },
|
||||||
|
{ value: 'AGV_FOLLOW_PATH', label: '底盘按路径导航' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const commonMotionFields = [
|
||||||
|
{ key: 'velocity', label: '速度', number: true, min: 0, step: 0.01, required: false },
|
||||||
|
{ key: 'acceleration', label: '加速度', number: true, min: 0, step: 0.01, required: false },
|
||||||
|
]
|
||||||
|
const fieldsByType = {
|
||||||
|
DELAY: [{ key: 'durationMs', label: '等待时长(毫秒)', number: true, min: 0 }],
|
||||||
|
ARM_MOVE_J: [{ key: 'positions', label: '关节位置(JSON 数组)', multiline: true }, ...commonMotionFields],
|
||||||
|
ARM_MOVE_L: [
|
||||||
|
...['x', 'y', 'z', 'rx', 'ry', 'rz'].map((key) => ({ key, label: key.toUpperCase(), number: true })),
|
||||||
|
{ key: 'frame', label: '坐标系', options: ['BASE', 'TOOL', 'WORLD', 'USER'].map((value) => ({ value, label: value })) },
|
||||||
|
...commonMotionFields,
|
||||||
|
],
|
||||||
|
AGV_NAVIGATE_TO_POSE: [
|
||||||
|
...['x', 'y', 'theta'].map((key) => ({ key, label: key === 'theta' ? '角度' : key.toUpperCase(), number: true })),
|
||||||
|
{ key: 'maxSpeed', label: '最大速度', number: true, min: 0, step: 0.01, required: false },
|
||||||
|
],
|
||||||
|
AGV_NAVIGATE_TO_STATION: [
|
||||||
|
{ key: 'stationId', label: '站点 ID' },
|
||||||
|
{ key: 'maxSpeed', label: '最大速度', number: true, min: 0, step: 0.01, required: false },
|
||||||
|
],
|
||||||
|
AGV_FOLLOW_PATH: [
|
||||||
|
{ key: 'path', label: '路径段(JSON 数组)', multiline: true },
|
||||||
|
{ key: 'maxSpeed', label: '最大速度', number: true, min: 0, step: 0.01, required: false },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
const defaultParams = {
|
||||||
|
DELAY: { durationMs: 1000 },
|
||||||
|
ARM_MOVE_J: { positions: '[0, 0, 0, 0, 0, 0]', velocity: 0.1, acceleration: 0.2 },
|
||||||
|
ARM_MOVE_L: { x: 0, y: 0, z: 0, rx: 0, ry: 0, rz: 0, frame: 'BASE', velocity: 0.1, acceleration: 0.2 },
|
||||||
|
AGV_NAVIGATE_TO_POSE: { x: 0, y: 0, theta: 0, maxSpeed: 0.5 },
|
||||||
|
AGV_NAVIGATE_TO_STATION: { stationId: '', maxSpeed: 0.5 },
|
||||||
|
AGV_FOLLOW_PATH: { path: '[]', maxSpeed: 0.5 },
|
||||||
|
}
|
||||||
|
|
||||||
|
const roles = ref([])
|
||||||
|
const formData = reactive({ actionId: '', totalTimeoutMs: 60000, steps: [], executionTarget: {} })
|
||||||
|
const fieldsForType = (type) => fieldsByType[type] || []
|
||||||
|
const deviceKindForType = (type) => type?.startsWith('ARM_') ? DEVICE_KIND.ARM : (type?.startsWith('AGV_') ? DEVICE_KIND.AGV : null)
|
||||||
|
const selectedRole = () => roles.value.find((role) => role.roleKey === formData.executionTarget.roleKey)
|
||||||
|
const matchingSlots = (type) => (selectedRole()?.deviceSlots || []).filter((slot) => Number(slot.deviceKind) === deviceKindForType(type))
|
||||||
|
|
||||||
|
const normalizeStep = (source = {}, index = 0) => {
|
||||||
|
const params = { ...(defaultParams[source.type || 'DELAY'] || {}), ...(source.params || {}) }
|
||||||
|
for (const key of ['positions', 'path']) {
|
||||||
|
if (Array.isArray(params[key])) params[key] = JSON.stringify(params[key])
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
localKey: source.localKey || createResourceKey('queue_step'),
|
||||||
|
stepId: source.stepId || `step_${index + 1}`,
|
||||||
|
type: source.type || 'DELAY',
|
||||||
|
timeoutMs: Number(source.timeoutMs || 0),
|
||||||
|
deviceBindingKey: source.deviceBindingKey || createResourceKey('queue_device'),
|
||||||
|
deviceSlotKey: source.deviceSlotKey || '',
|
||||||
|
params,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const initData = () => {
|
||||||
|
roles.value = getResourceRoles()
|
||||||
|
const properties = props.data.properties || {}
|
||||||
|
const params = Object.fromEntries((properties.nodeParams || []).map((item) => [item.name, item.input]))
|
||||||
|
formData.actionId = params.actionId || ''
|
||||||
|
formData.totalTimeoutMs = Number(params.totalTimeoutMs || 60000)
|
||||||
|
formData.steps = (Array.isArray(params.steps) ? params.steps : []).map(normalizeStep)
|
||||||
|
formData.executionTarget = JSON.parse(JSON.stringify(properties.executionTarget || {}))
|
||||||
|
formData.executionTarget.roleKey ||= roles.value[0]?.roleKey || ''
|
||||||
|
formData.steps.forEach((step) => ensureStepSlot(step, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
const persistRoles = () => {
|
||||||
|
const graph = lf.getGraphData()
|
||||||
|
const start = getStartNode(graph)
|
||||||
|
if (!start) return
|
||||||
|
lf.setProperties(start.id, { ...start.properties, resourceRoles: JSON.parse(JSON.stringify(roles.value)) })
|
||||||
|
}
|
||||||
|
|
||||||
|
const ensureStepSlot = (step, createWhenMissing = true) => {
|
||||||
|
const kind = deviceKindForType(step.type)
|
||||||
|
if (!kind) {
|
||||||
|
step.deviceSlotKey = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const role = selectedRole()
|
||||||
|
if (!role) return
|
||||||
|
const options = matchingSlots(step.type)
|
||||||
|
if (options.some((slot) => slot.slotKey === step.deviceSlotKey)) return
|
||||||
|
let slot = options[0]
|
||||||
|
if (!slot && createWhenMissing) {
|
||||||
|
slot = createDeviceSlot(kind, role.deviceSlots || [])
|
||||||
|
slot.managedBy = FLOW_NODE_RESOURCE_OWNER
|
||||||
|
role.deviceSlots ||= []
|
||||||
|
role.deviceSlots.push(slot)
|
||||||
|
persistRoles()
|
||||||
|
}
|
||||||
|
step.deviceSlotKey = slot?.slotKey || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const addStep = () => formData.steps.push(normalizeStep({}, formData.steps.length))
|
||||||
|
const removeStep = (index) => formData.steps.splice(index, 1)
|
||||||
|
const moveStep = (index, offset) => {
|
||||||
|
const [step] = formData.steps.splice(index, 1)
|
||||||
|
formData.steps.splice(index + offset, 0, step)
|
||||||
|
}
|
||||||
|
const handleStepTypeChange = (step) => {
|
||||||
|
step.params = { ...(defaultParams[step.type] || {}) }
|
||||||
|
ensureStepSlot(step)
|
||||||
|
}
|
||||||
|
const handleRoleChange = () => formData.steps.forEach((step) => ensureStepSlot(step))
|
||||||
|
|
||||||
|
const parseJsonFields = (step) => {
|
||||||
|
const params = { ...step.params }
|
||||||
|
for (const key of ['positions', 'path']) {
|
||||||
|
if (typeof params[key] !== 'string') continue
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(params[key])
|
||||||
|
if (!Array.isArray(parsed)) throw new Error()
|
||||||
|
params[key] = parsed
|
||||||
|
} catch {
|
||||||
|
throw new Error(`${fieldsForType(step.type).find((field) => field.key === key)?.label || key}格式不正确`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateAndSave = async () => {
|
||||||
|
try {
|
||||||
|
if (!formData.executionTarget.roleKey) throw new Error('请选择执行机器人角色')
|
||||||
|
if (!formData.steps.length) throw new Error('请添加至少一个动作步骤')
|
||||||
|
const steps = formData.steps.map((step, index) => {
|
||||||
|
if (!step.type) throw new Error(`步骤 ${index + 1} 未选择动作`)
|
||||||
|
if (deviceKindForType(step.type) && !step.deviceSlotKey) throw new Error(`步骤 ${index + 1} 未选择设备用途`)
|
||||||
|
const params = parseJsonFields(step)
|
||||||
|
fieldsForType(step.type).filter((field) => field.required !== false).forEach((field) => {
|
||||||
|
if (params[field.key] === '' || params[field.key] == null) throw new Error(`步骤 ${index + 1} 的${field.label}不能为空`)
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
stepId: `step_${index + 1}`,
|
||||||
|
type: step.type,
|
||||||
|
timeoutMs: Number(step.timeoutMs || 0),
|
||||||
|
...(deviceKindForType(step.type) ? {
|
||||||
|
deviceBindingKey: step.deviceBindingKey,
|
||||||
|
deviceSlotKey: step.deviceSlotKey,
|
||||||
|
} : {}),
|
||||||
|
params,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const executionTarget = {
|
||||||
|
roleKey: formData.executionTarget.roleKey,
|
||||||
|
dispatchPolicy: 'ONE',
|
||||||
|
deviceBindings: steps.filter((step) => step.deviceBindingKey).map((step) => ({
|
||||||
|
parameterName: step.deviceBindingKey,
|
||||||
|
slotKey: step.deviceSlotKey,
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
lf.setProperties(props.data.id, {
|
||||||
|
...props.data.properties,
|
||||||
|
executionTarget,
|
||||||
|
nodeParams: [
|
||||||
|
{ name: 'actionId', type: 'input', input: formData.actionId, required: false },
|
||||||
|
{ name: 'totalTimeoutMs', type: 'input', componentType: 'number', input: formData.totalTimeoutMs, required: true },
|
||||||
|
{ name: 'steps', type: 'input', input: steps, required: true },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
const cleanup = pruneUnusedNodeResources(lf.getGraphData())
|
||||||
|
if (cleanup.start) {
|
||||||
|
lf.setProperties(cleanup.start.id, {
|
||||||
|
...cleanup.start.properties,
|
||||||
|
resourceRoles: cleanup.start.properties.resourceRoles,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
emit('save-success')
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.warning(error.message)
|
||||||
|
emit('save-error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => props.data, initData, { immediate: true, deep: true })
|
||||||
|
defineExpose({ validateAndSave })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.queue-editor { padding-top: 16px; }
|
||||||
|
.queue-editor__heading { color: #344054; font-size: 14px; font-weight: 650; }
|
||||||
|
.queue-editor__hint { margin-top: 3px; color: #7a8494; font-size: 12px; }
|
||||||
|
.queue-editor__toolbar { display: flex; margin: 14px 0 12px; align-items: center; justify-content: space-between; }
|
||||||
|
.queue-step { margin-bottom: 12px; padding: 14px; border: 1px solid #dfe4ec; border-radius: 8px; background: #fff; }
|
||||||
|
.queue-step__header { display: flex; margin-bottom: 12px; align-items: center; justify-content: space-between; color: #1f2937; font-weight: 650; }
|
||||||
|
.queue-step__header .el-button + .el-button { margin-left: 6px; }
|
||||||
|
.queue-editor__warning { color: var(--el-color-warning); font-size: 12px; }
|
||||||
|
</style>
|
||||||
@ -337,6 +337,25 @@ function handler(params) {
|
|||||||
outputParams: [],
|
outputParams: [],
|
||||||
outputType: 'json'
|
outputType: 'json'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
icon: planSvg,
|
||||||
|
name: "动作队列",
|
||||||
|
type: "actionQueue",
|
||||||
|
desc: "在边缘端按顺序连续执行多个动作",
|
||||||
|
action: 'SYSTEM_EXECUTE_ACTION_QUEUE',
|
||||||
|
nodeType: 'EDGE',
|
||||||
|
nodeParams: [
|
||||||
|
{ name: "actionId", type: "input", input: "", required: false },
|
||||||
|
{ name: "totalTimeoutMs", type: "input", componentType: 'number', input: 60000, required: true },
|
||||||
|
{ name: "steps", type: "input", input: [], required: true },
|
||||||
|
],
|
||||||
|
outputParams: [
|
||||||
|
{ name: 'actionId', type: 'string', desc: '动作队列标识' },
|
||||||
|
{ name: 'completedSteps', type: 'number', desc: '已完成步骤数' },
|
||||||
|
{ name: 'result', type: 'string', desc: '执行结果' },
|
||||||
|
],
|
||||||
|
outputType: 'json'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
icon: videoSvg,
|
icon: videoSvg,
|
||||||
name: "机械臂",
|
name: "机械臂",
|
||||||
|
|||||||
@ -159,7 +159,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-dialog v-model="visible" width="500" class="single-node-dialog" append-to-body>
|
<el-dialog v-model="visible" :width="nodeAction === 'SYSTEM_EXECUTE_ACTION_QUEUE' ? '620' : '500'" class="single-node-dialog" append-to-body>
|
||||||
<template #header="{ close, titleId, titleClass }">
|
<template #header="{ close, titleId, titleClass }">
|
||||||
<div class="my-header">
|
<div class="my-header">
|
||||||
<h4 :id="titleId" :class="titleClass">执行{{ nodeName }}节点</h4>
|
<h4 :id="titleId" :class="titleClass">执行{{ nodeName }}节点</h4>
|
||||||
@ -175,7 +175,15 @@
|
|||||||
>加载站点列表</el-button>
|
>加载站点列表</el-button>
|
||||||
<span>{{ singleNodeRobotId && singleNodeDeviceId ? '加载后从站点下拉框选择' : '请先选择机器人和设备' }}</span>
|
<span>{{ singleNodeRobotId && singleNodeDeviceId ? '加载后从站点下拉框选择' : '请先选择机器人和设备' }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<ActionQueueSingleRun
|
||||||
|
v-if="nodeAction === 'SYSTEM_EXECUTE_ACTION_QUEUE'"
|
||||||
|
ref="actionQueueSingleRunRef"
|
||||||
|
:node-params="formData.nodeParams"
|
||||||
|
:robot-options="singleNodeRobotOptions"
|
||||||
|
:initial-robot-id="singleNodeInitialRobotId"
|
||||||
|
/>
|
||||||
<el-form
|
<el-form
|
||||||
|
v-else
|
||||||
:inline="true"
|
:inline="true"
|
||||||
:model="formData"
|
:model="formData"
|
||||||
:rules="rules"
|
:rules="rules"
|
||||||
@ -264,6 +272,7 @@ import { useFlowEvents } from "./composables/useFlowEvents";
|
|||||||
import TestRun from "./components/TestRun.vue";
|
import TestRun from "./components/TestRun.vue";
|
||||||
import ParamsDrawer from "./components/ParamsDrawer.vue";
|
import ParamsDrawer from "./components/ParamsDrawer.vue";
|
||||||
import Aside from "./components/Aside.vue";
|
import Aside from "./components/Aside.vue";
|
||||||
|
import ActionQueueSingleRun from "./components/ActionQueueSingleRun.vue";
|
||||||
import { ElMessage } from "element-plus";
|
import { ElMessage } from "element-plus";
|
||||||
import { useFlowStore } from "@/store/modules/flow";
|
import { useFlowStore } from "@/store/modules/flow";
|
||||||
import { emitter } from "@/utils/eventBus";
|
import { emitter } from "@/utils/eventBus";
|
||||||
@ -1024,6 +1033,8 @@ const singleNodeRobotCache = new Map();
|
|||||||
let lastSingleNodeRobotId = "";
|
let lastSingleNodeRobotId = "";
|
||||||
const stationLoading = ref(false);
|
const stationLoading = ref(false);
|
||||||
const singleNodeRobotOptions = ref([]);
|
const singleNodeRobotOptions = ref([]);
|
||||||
|
const actionQueueSingleRunRef = ref();
|
||||||
|
const singleNodeInitialRobotId = ref("");
|
||||||
const flattenSingleNodeParams = (params, parentPath = [], fields = []) => {
|
const flattenSingleNodeParams = (params, parentPath = [], fields = []) => {
|
||||||
(params || []).forEach((param, index) => {
|
(params || []).forEach((param, index) => {
|
||||||
const name = String(param?.name || "").trim();
|
const name = String(param?.name || "").trim();
|
||||||
@ -1070,9 +1081,12 @@ const singleNodeDeviceId = computed(() =>
|
|||||||
);
|
);
|
||||||
// 在执行、关闭弹窗或切换节点前记录当前机器人选择。
|
// 在执行、关闭弹窗或切换节点前记录当前机器人选择。
|
||||||
const rememberSingleNodeRobot = () => {
|
const rememberSingleNodeRobot = () => {
|
||||||
if (!activeSingleNodeId.value || !singleNodeRobotId.value) return;
|
const selectedRobotId = nodeAction.value === "SYSTEM_EXECUTE_ACTION_QUEUE"
|
||||||
lastSingleNodeRobotId = singleNodeRobotId.value;
|
? actionQueueSingleRunRef.value?.getRobotId?.()
|
||||||
singleNodeRobotCache.set(activeSingleNodeId.value, singleNodeRobotId.value);
|
: singleNodeRobotId.value;
|
||||||
|
if (!activeSingleNodeId.value || !selectedRobotId) return;
|
||||||
|
lastSingleNodeRobotId = selectedRobotId;
|
||||||
|
singleNodeRobotCache.set(activeSingleNodeId.value, selectedRobotId);
|
||||||
};
|
};
|
||||||
|
|
||||||
watch(visible, (value) => {
|
watch(visible, (value) => {
|
||||||
@ -1186,6 +1200,12 @@ const singleNodeExecution = async (e) => {
|
|||||||
stationLoading.value = false;
|
stationLoading.value = false;
|
||||||
if (effectiveNodeType === "EDGE") {
|
if (effectiveNodeType === "EDGE") {
|
||||||
await loadSingleNodeRobots();
|
await loadSingleNodeRobots();
|
||||||
|
const cachedRobotId = singleNodeRobotCache.get(activeSingleNodeId.value) || lastSingleNodeRobotId;
|
||||||
|
if (normalizedAction === "SYSTEM_EXECUTE_ACTION_QUEUE") {
|
||||||
|
formData.nodeParams = executionParams;
|
||||||
|
singleNodeInitialRobotId.value = cachedRobotId || "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
const routingParams = [
|
const routingParams = [
|
||||||
{
|
{
|
||||||
disabled: true,
|
disabled: true,
|
||||||
@ -1207,7 +1227,6 @@ const singleNodeExecution = async (e) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
formData.nodeParams = [...routingParams, ...executionParams];
|
formData.nodeParams = [...routingParams, ...executionParams];
|
||||||
const cachedRobotId = singleNodeRobotCache.get(activeSingleNodeId.value) || lastSingleNodeRobotId;
|
|
||||||
if (cachedRobotId) {
|
if (cachedRobotId) {
|
||||||
formData.nodeParams[0].input = cachedRobotId;
|
formData.nodeParams[0].input = cachedRobotId;
|
||||||
await handleSingleNodeRobotChange(cachedRobotId);
|
await handleSingleNodeRobotChange(cachedRobotId);
|
||||||
@ -1228,6 +1247,21 @@ const singleNodeExecution = async (e) => {
|
|||||||
* 执行 execute 对应的数据或交互;作用范围仅限当前组件或模块。
|
* 执行 execute 对应的数据或交互;作用范围仅限当前组件或模块。
|
||||||
*/
|
*/
|
||||||
const execute = async () => {
|
const execute = async () => {
|
||||||
|
if (nodeAction.value === "SYSTEM_EXECUTE_ACTION_QUEUE") {
|
||||||
|
if (!actionQueueSingleRunRef.value?.validate?.()) return;
|
||||||
|
rememberSingleNodeRobot();
|
||||||
|
singleNodeExecuting.value = true;
|
||||||
|
try {
|
||||||
|
const res = await flowAction({ action: nodeAction.value, payload: actionQueueSingleRunRef.value.getPayload() });
|
||||||
|
if (res.code === 200) ElMessage.success("执行成功");
|
||||||
|
else ElMessage.error(res.msg);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("动作队列单节点执行失败:", error);
|
||||||
|
} finally {
|
||||||
|
singleNodeExecuting.value = false;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (nodeAction.value === "AGV_MOVE_TO_STATION") {
|
if (nodeAction.value === "AGV_MOVE_TO_STATION") {
|
||||||
const stationParam = formData.nodeParams.find((item) => item.name === "stationId");
|
const stationParam = formData.nodeParams.find((item) => item.name === "stationId");
|
||||||
const validStation = stationParam?.selectOptions?.some((item) => item.value === stationParam.input);
|
const validStation = stationParam?.selectOptions?.some((item) => item.value === stationParam.input);
|
||||||
|
|||||||
25
src/views/flow/nodes/function/actionQueue.vue
Normal file
25
src/views/flow/nodes/function/actionQueue.vue
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<template>
|
||||||
|
<div class="action-queue-node">
|
||||||
|
<ServiceNode ref="serviceNodeRef" v-bind="props" @content-change="$emit('contentChange')" />
|
||||||
|
<div class="action-queue-node__summary">{{ stepCount }} 个步骤</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import ServiceNode from '../common/serviceNode.vue'
|
||||||
|
|
||||||
|
const props = defineProps({ model: Object, properties: Object })
|
||||||
|
defineEmits(['contentChange'])
|
||||||
|
const serviceNodeRef = ref()
|
||||||
|
const stepCount = computed(() => {
|
||||||
|
const steps = props.properties?.nodeParams?.find((item) => item.name === 'steps')?.input
|
||||||
|
return Array.isArray(steps) ? steps.length : 0
|
||||||
|
})
|
||||||
|
const closePopover = () => serviceNodeRef.value?.closePopover?.()
|
||||||
|
defineExpose({ closePopover })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.action-queue-node__summary { margin-top: 7px; color: #667085; font-size: 11px; line-height: 16px; }
|
||||||
|
</style>
|
||||||
@ -80,6 +80,7 @@ import CurrentLoopNode from "../nodes/function/currentLoop.vue";
|
|||||||
import SdAgentNode from "../nodes/function/sdAgent.vue";
|
import SdAgentNode from "../nodes/function/sdAgent.vue";
|
||||||
import RecognizeNode from "../nodes/function/recognize.vue";
|
import RecognizeNode from "../nodes/function/recognize.vue";
|
||||||
import DeviceUniversal from "../nodes/function/deviceUniversal.vue";
|
import DeviceUniversal from "../nodes/function/deviceUniversal.vue";
|
||||||
|
import ActionQueue from "../nodes/function/actionQueue.vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
id: String,
|
id: String,
|
||||||
@ -106,6 +107,7 @@ const componentMap = {
|
|||||||
recognize: RecognizeNode,
|
recognize: RecognizeNode,
|
||||||
deviceUniversal: DeviceUniversal,
|
deviceUniversal: DeviceUniversal,
|
||||||
device_universal: DeviceUniversal,
|
device_universal: DeviceUniversal,
|
||||||
|
actionQueue: ActionQueue,
|
||||||
};
|
};
|
||||||
|
|
||||||
const flowStore = useFlowStore();
|
const flowStore = useFlowStore();
|
||||||
@ -120,6 +122,8 @@ const executionLabel = computed(() => {
|
|||||||
const start = graph?.nodes?.find((node) => node.type === "start");
|
const start = graph?.nodes?.find((node) => node.type === "start");
|
||||||
const role = start?.properties?.resourceRoles?.find((item) => item.roleKey === roleKey);
|
const role = start?.properties?.resourceRoles?.find((item) => item.roleKey === roleKey);
|
||||||
if (!role) return "未配置机器人角色";
|
if (!role) return "未配置机器人角色";
|
||||||
|
const bindings = properties.value.executionTarget?.deviceBindings || [];
|
||||||
|
if (bindings.length > 1) return `${role.displayName} · ${bindings.length} 个设备用途`;
|
||||||
const slotKey = properties.value.executionTarget?.deviceSlotKey
|
const slotKey = properties.value.executionTarget?.deviceSlotKey
|
||||||
|| properties.value.executionTarget?.deviceBindings?.[0]?.slotKey;
|
|| properties.value.executionTarget?.deviceBindings?.[0]?.slotKey;
|
||||||
const slot = role.deviceSlots?.find((item) => item.slotKey === slotKey);
|
const slot = role.deviceSlots?.find((item) => item.slotKey === slotKey);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user