2026-08-11 16:50:30 +08:00
|
|
|
|
<!-- 开始节点维护流程级机器人角色和业务输入参数。 -->
|
2026-07-06 11:11:59 +08:00
|
|
|
|
<template>
|
2026-08-11 16:50:30 +08:00
|
|
|
|
<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>
|
2026-07-06 11:11:59 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
2026-08-11 16:50:30 +08:00
|
|
|
|
import { reactive, ref, watch } from 'vue'
|
|
|
|
|
|
import { Delete, Minus, Plus } from '@element-plus/icons-vue'
|
|
|
|
|
|
import { ElMessage } from 'element-plus'
|
2026-07-06 11:11:59 +08:00
|
|
|
|
import FormItemRecursive from '../FormItemRecursive.vue'
|
2026-08-11 16:50:30 +08:00
|
|
|
|
import {
|
|
|
|
|
|
createDefaultRobotRole,
|
|
|
|
|
|
createDeviceSlot,
|
|
|
|
|
|
createResourceKey,
|
|
|
|
|
|
normalizeFlowResourceSchema,
|
2026-08-11 17:16:27 +08:00
|
|
|
|
USER_RESOURCE_OWNER,
|
2026-08-11 16:50:30 +08:00
|
|
|
|
} from '@/utils/flowResources'
|
|
|
|
|
|
|
|
|
|
|
|
const props = defineProps({ data: Object })
|
|
|
|
|
|
const emit = defineEmits(['save-success', 'save-error'])
|
2026-07-06 11:11:59 +08:00
|
|
|
|
|
2026-08-11 16:50:30 +08:00
|
|
|
|
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}`,
|
2026-08-11 17:16:27 +08:00
|
|
|
|
managedBy: USER_RESOURCE_OWNER,
|
2026-08-11 16:50:30 +08:00
|
|
|
|
})
|
|
|
|
|
|
}
|
2026-07-06 11:11:59 +08:00
|
|
|
|
|
2026-08-11 16:50:30 +08:00
|
|
|
|
const addDeviceSlot = (role) => {
|
2026-08-11 17:16:27 +08:00
|
|
|
|
role.deviceSlots.push({
|
|
|
|
|
|
...createDeviceSlot(null, role.deviceSlots),
|
|
|
|
|
|
managedBy: USER_RESOURCE_OWNER,
|
|
|
|
|
|
})
|
2026-08-11 16:50:30 +08:00
|
|
|
|
}
|
2026-07-06 11:11:59 +08:00
|
|
|
|
|
2026-08-11 16:50:30 +08:00
|
|
|
|
const referencedNodes = (predicate) => globalThis.lf.getGraphData().nodes.filter((node) => predicate(node.properties?.executionTarget))
|
2026-07-06 11:11:59 +08:00
|
|
|
|
|
2026-08-11 16:50:30 +08:00
|
|
|
|
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)
|
|
|
|
|
|
}
|
2026-07-06 11:11:59 +08:00
|
|
|
|
|
2026-08-11 16:50:30 +08:00
|
|
|
|
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)
|
2026-07-06 11:11:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-11 16:50:30 +08:00
|
|
|
|
const addFormItem = (event, type) => {
|
|
|
|
|
|
event.stopPropagation()
|
|
|
|
|
|
formData[type].push({ name: '', type: 'string', required: false, children: [], input: '' })
|
2026-07-06 11:11:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const deleteTopLevelItem = (fullPath, propPath) => {
|
2026-08-11 16:50:30 +08:00
|
|
|
|
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)
|
2026-07-06 11:11:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const initData = () => {
|
2026-08-11 16:50:30 +08:00
|
|
|
|
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 || []))
|
2026-07-06 11:11:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-11 16:50:30 +08:00
|
|
|
|
watch(() => props.data.id, initData, { immediate: true })
|
|
|
|
|
|
|
2026-07-06 11:11:59 +08:00
|
|
|
|
const validateAndSave = async () => {
|
2026-08-11 16:50:30 +08:00
|
|
|
|
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')
|
2026-07-06 11:11:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
defineExpose({ validateAndSave })
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
2026-08-11 16:50:30 +08:00
|
|
|
|
<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;
|
2026-07-06 11:11:59 +08:00
|
|
|
|
}
|
2026-08-07 14:38:16 +08:00
|
|
|
|
</style>
|