CMVR-IOT-UI/src/views/flow/components/params/StartParams.vue
lixiaolong e3a2761683 fix(flow): 解决非机器人节点携带机器人执行目标导致的后端误判问题
- 非机器人节点不再携带 executionTarget 属性,避免后端将空对象误判为无效角色引用
- 添加 FLOW_NODE_RESOURCE_OWNER 和 USER_RESOURCE_OWNER 常量用于资源归属管理
- 为流程节点创建的设备插槽添加 managedBy 字段标识资源归属
- 移除未被任何节点引用的自动注册角色定义和设备使用
- 为用户手动创建的角色和设备插槽标记为 USER_RESOURCE_OWNER 归属
- 在节点更新时自动清理未使用的节点资源,优化资源管理
2026-08-11 17:16:27 +08:00

233 lines
7.7 KiB
Vue

<!-- 开始节点维护流程级机器人角色和业务输入参数。 -->
<template>
<el-collapse v-model="activeNames">
<el-collapse-item name="roles" title="机器人角色">
<div class="role-intro">节点按角色分工,试运行时再安排具体机器人和设备。</div>
<div v-for="(role, roleIndex) in formData.resourceRoles" :key="role.roleKey" class="role-row">
<div class="role-row__header">
<el-input v-model="role.displayName" maxlength="30" placeholder="例如:巡检机器人" />
<el-button
circle
:icon="Delete"
:disabled="formData.resourceRoles.length === 1"
aria-label="删除机器人角色"
@click="deleteRole(roleIndex)"
/>
</div>
<div class="role-row__section">
<div class="role-row__label">设备用途</div>
<div v-if="!role.deviceSlots.length" class="empty-tip">当前角色暂不需要设备</div>
<div v-for="(slot, slotIndex) in role.deviceSlots" :key="slot.slotKey" class="device-row">
<el-input v-model="slot.displayName" maxlength="30" placeholder="例如:前方摄像头" />
<el-select v-model="slot.deviceKind" placeholder="设备类型">
<el-option v-for="item in deviceKinds" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<el-button circle :icon="Minus" aria-label="删除设备用途" @click="deleteDeviceSlot(role, slotIndex)" />
</div>
<el-button text type="primary" :icon="Plus" @click="addDeviceSlot(role)">添加设备用途</el-button>
</div>
</div>
<el-button type="primary" plain :icon="Plus" @click="addRole">添加机器人角色</el-button>
</el-collapse-item>
<el-collapse-item name="inputs">
<template #title>
输入参数
<el-button circle size="small" class="addFormItem" @click="(event) => addFormItem(event, 'inputParams')">
<el-icon :size="14"><Plus /></el-icon>
</el-button>
</template>
<el-form
ref="dynamicFormRef"
:inline="true"
:model="formData"
label-position="top"
label-width="auto"
>
<FormItemRecursive
formType="input"
:current-list="formData.inputParams"
prop-path="inputParams"
:depth="0"
:is-first-level="true"
:endDepth="2"
:parent-path="[]"
@delete-item="(path) => deleteTopLevelItem(path, 'inputParams')"
/>
</el-form>
</el-collapse-item>
</el-collapse>
</template>
<script setup>
import { reactive, ref, watch } from 'vue'
import { Delete, Minus, Plus } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import FormItemRecursive from '../FormItemRecursive.vue'
import {
createDefaultRobotRole,
createDeviceSlot,
createResourceKey,
normalizeFlowResourceSchema,
USER_RESOURCE_OWNER,
} from '@/utils/flowResources'
const props = defineProps({ data: Object })
const emit = defineEmits(['save-success', 'save-error'])
const deviceKinds = [
{ label: '移动底盘', value: 1 },
{ label: '机械臂', value: 2 },
{ label: '仿生头', value: 4 },
{ label: '摄像头', value: 5 },
{ label: '麦克风', value: 9 },
{ label: '机器人本体', value: 12 },
{ label: '扬声器', value: 13 },
]
const formData = reactive({ resourceRoles: [], inputParams: [] })
const dynamicFormRef = ref()
const activeNames = ref(['roles', 'inputs'])
const addRole = () => {
formData.resourceRoles.push({
...createDefaultRobotRole(),
roleKey: createResourceKey('robot_role'),
displayName: `机器人角色${formData.resourceRoles.length + 1}`,
managedBy: USER_RESOURCE_OWNER,
})
}
const addDeviceSlot = (role) => {
role.deviceSlots.push({
...createDeviceSlot(null, role.deviceSlots),
managedBy: USER_RESOURCE_OWNER,
})
}
const referencedNodes = (predicate) => globalThis.lf.getGraphData().nodes.filter((node) => predicate(node.properties?.executionTarget))
const deleteRole = (index) => {
const role = formData.resourceRoles[index]
const references = referencedNodes((target) => target?.roleKey === role.roleKey)
if (references.length) {
ElMessage.warning(`“${role.displayName}”仍被 ${references.length} 个节点使用,请先调整这些节点`)
return
}
formData.resourceRoles.splice(index, 1)
}
const deleteDeviceSlot = (role, index) => {
const slot = role.deviceSlots[index]
const references = referencedNodes((target) => (
target?.roleKey === role.roleKey
&& (target.deviceSlotKey === slot.slotKey
|| target.deviceBindings?.some((binding) => binding.slotKey === slot.slotKey))
))
if (references.length) {
ElMessage.warning(`“${slot.displayName}”仍被 ${references.length} 个节点使用,请先调整这些节点`)
return
}
role.deviceSlots.splice(index, 1)
}
const addFormItem = (event, type) => {
event.stopPropagation()
formData[type].push({ name: '', type: 'string', required: false, children: [], input: '' })
}
const deleteTopLevelItem = (fullPath, propPath) => {
let currentLevel = formData[propPath]
for (let index = 0; index < fullPath.length - 1; index += 1) {
currentLevel = currentLevel[fullPath[index]].children
}
currentLevel.splice(fullPath[fullPath.length - 1], 1)
}
const initData = () => {
const graph = normalizeFlowResourceSchema(globalThis.lf.getGraphData())
const start = graph.nodes.find((node) => node.id === props.data.id)
formData.resourceRoles = JSON.parse(JSON.stringify(start?.properties?.resourceRoles || [createDefaultRobotRole()]))
formData.inputParams = JSON.parse(JSON.stringify(start?.properties?.inputParams || []))
}
watch(() => props.data.id, initData, { immediate: true })
const validateAndSave = async () => {
const names = formData.resourceRoles.map((role) => role.displayName.trim())
const invalidRole = formData.resourceRoles.some((role) => !role.displayName.trim())
const duplicateRole = new Set(names).size !== names.length
const invalidSlot = formData.resourceRoles.some((role) => role.deviceSlots.some((slot) => (
!slot.displayName.trim() || slot.deviceKind == null
)))
if (invalidRole || duplicateRole || invalidSlot) {
ElMessage.warning(invalidRole ? '请填写机器人角色名称' : duplicateRole ? '机器人角色名称不能重复' : '请完整填写设备用途和类型')
emit('save-error')
return
}
let inputValid = true
if (dynamicFormRef.value) {
await dynamicFormRef.value.validate((valid) => { if (!valid) inputValid = false })
}
globalThis.lf.setProperties(props.data.id, {
...props.data.properties,
schemaVersion: 2,
resourceRoles: JSON.parse(JSON.stringify(formData.resourceRoles)),
inputParams: JSON.parse(JSON.stringify(formData.inputParams)),
})
emit(inputValid ? 'save-success' : 'save-error')
}
defineExpose({ validateAndSave })
</script>
<style scoped lang="scss">
.role-intro {
margin-bottom: 14px;
color: var(--el-text-color-secondary);
font-size: 13px;
line-height: 20px;
}
.role-row {
margin-bottom: 14px;
padding: 14px;
border: 1px solid var(--el-border-color-light);
border-radius: 6px;
}
.role-row__header,
.device-row {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 10px;
align-items: center;
}
.role-row__section {
margin-top: 14px;
padding-top: 12px;
border-top: 1px solid var(--el-border-color-lighter);
}
.role-row__label {
margin-bottom: 8px;
color: var(--el-text-color-regular);
font-size: 13px;
font-weight: 600;
}
.device-row {
grid-template-columns: minmax(0, 1fr) 132px auto;
margin-bottom: 8px;
}
.empty-tip {
margin-bottom: 6px;
color: var(--el-text-color-placeholder);
font-size: 12px;
}
</style>