Compare commits
3 Commits
e78df2ed5d
...
bbc937ce94
| Author | SHA1 | Date | |
|---|---|---|---|
| bbc937ce94 | |||
| 289dd678d0 | |||
| 0df5280ebf |
@ -38,10 +38,11 @@ export function deleteRunTask(id) {
|
||||
})
|
||||
}
|
||||
|
||||
export function startRunTask(id) {
|
||||
export function startRunTask(id, data) {
|
||||
return request({
|
||||
url: `/inspection/taskInstance/start/${id}`,
|
||||
method: 'put'
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
@ -72,4 +73,4 @@ export function getTaskLogListApi(params) {
|
||||
method: 'get',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -32,10 +32,11 @@ export function deleteTaskinstance(ids) {
|
||||
})
|
||||
}
|
||||
|
||||
export function startTaskinstance(id) {
|
||||
export function startTaskinstance(id, data) {
|
||||
return request({
|
||||
url: `/aima/taskinstance/start/${id}`,
|
||||
method: 'put'
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
@ -66,4 +67,4 @@ export function getTaskLogListApi(params) {
|
||||
method: 'get',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
261
src/utils/flowResources.js
Normal file
261
src/utils/flowResources.js
Normal file
@ -0,0 +1,261 @@
|
||||
import { getDeviceKindForAction } from '@/utils/robotDevice'
|
||||
|
||||
export const FLOW_SCHEMA_VERSION = 2
|
||||
export const DEFAULT_ROLE_KEY = 'default_executor'
|
||||
|
||||
const DEVICE_KIND_META = Object.freeze({
|
||||
1: { key: 'agv', name: '移动底盘' },
|
||||
2: { key: 'arm', name: '机械臂' },
|
||||
4: { key: 'bio_head', name: '仿生头' },
|
||||
5: { key: 'camera', name: '摄像头' },
|
||||
9: { key: 'microphone', name: '麦克风' },
|
||||
12: { key: 'robot', name: '机器人本体' },
|
||||
13: { key: 'speaker', name: '扬声器' },
|
||||
})
|
||||
|
||||
const clone = (value) => JSON.parse(JSON.stringify(value ?? null))
|
||||
const normalizedText = (value) => String(value || '').trim()
|
||||
|
||||
export const createResourceKey = (prefix) => {
|
||||
const random = globalThis.crypto?.randomUUID?.().replace(/-/g, '').slice(0, 10)
|
||||
|| `${Date.now()}${Math.random().toString(16).slice(2, 8)}`
|
||||
return `${prefix}_${random}`
|
||||
}
|
||||
|
||||
export const createDefaultRobotRole = (legacyRobotId = '') => ({
|
||||
roleKey: DEFAULT_ROLE_KEY,
|
||||
resourceType: 'ROBOT',
|
||||
displayName: '执行机器人',
|
||||
required: true,
|
||||
quantity: { min: 1, max: 1 },
|
||||
deviceSlots: [],
|
||||
...(legacyRobotId ? { legacyRobotId } : {}),
|
||||
})
|
||||
|
||||
export const createDeviceSlot = (deviceKind, existingSlots = [], displayName = '') => {
|
||||
const meta = DEVICE_KIND_META[Number(deviceKind)] || { key: 'device', name: '执行设备' }
|
||||
const usedKeys = new Set(existingSlots.map((slot) => slot.slotKey))
|
||||
let index = 1
|
||||
let slotKey = meta.key
|
||||
while (usedKeys.has(slotKey)) slotKey = `${meta.key}_${++index}`
|
||||
const sameKindCount = existingSlots.filter((slot) => Number(slot.deviceKind) === Number(deviceKind)).length
|
||||
return {
|
||||
slotKey,
|
||||
displayName: displayName || `${meta.name}${sameKindCount ? sameKindCount + 1 : ''}`,
|
||||
deviceKind: deviceKind == null ? null : Number(deviceKind),
|
||||
required: true,
|
||||
allowReuse: false,
|
||||
}
|
||||
}
|
||||
|
||||
export const getStartNode = (graph = globalThis.lf?.getGraphData?.()) => (
|
||||
graph?.nodes?.find((node) => node.type === 'start')
|
||||
)
|
||||
|
||||
export const getResourceRoles = (graph = globalThis.lf?.getGraphData?.()) => (
|
||||
clone(getStartNode(graph)?.properties?.resourceRoles || []) || []
|
||||
)
|
||||
|
||||
const isRobotNode = (node) => node?.properties?.nodeType === 'EDGE'
|
||||
export const actionRequiresDevice = (action) => (
|
||||
getDeviceKindForAction(action) != null || action === 'DEVICE_EXECUTE_JSON_COMMAND'
|
||||
)
|
||||
export const nodeRequiresDevice = (node) => actionRequiresDevice(node?.properties?.action)
|
||||
|
||||
const resolveLegacyRobotId = (startProperties) => (
|
||||
startProperties?.inputParams?.find((item) => item.name === 'robotId')?.input
|
||||
|| startProperties?.robotId
|
||||
|| ''
|
||||
)
|
||||
|
||||
const findOrCreateLegacySlot = (role, node, legacySlotByIdentity, legacyDeviceId = '') => {
|
||||
const kind = getDeviceKindForAction(node.properties?.action)
|
||||
const normalizedDeviceId = normalizedText(legacyDeviceId)
|
||||
const identity = `${role.roleKey}:${kind ?? 'any'}:${normalizedDeviceId || 'default'}`
|
||||
if (legacySlotByIdentity.has(identity)) return legacySlotByIdentity.get(identity)
|
||||
|
||||
const slot = createDeviceSlot(kind, role.deviceSlots)
|
||||
if (normalizedDeviceId) slot.legacyDeviceId = normalizedDeviceId
|
||||
role.deviceSlots.push(slot)
|
||||
legacySlotByIdentity.set(identity, slot)
|
||||
return slot
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads old graphs as Schema V2 without mutating the source object. Physical IDs are
|
||||
* retained only as migration hints and are consumed by the trial-run assignment form.
|
||||
*/
|
||||
export const normalizeFlowResourceSchema = (sourceGraph) => {
|
||||
const graph = clone(sourceGraph || { nodes: [], edges: [] })
|
||||
const start = getStartNode(graph)
|
||||
if (!start) return graph
|
||||
|
||||
const startProperties = start.properties || (start.properties = {})
|
||||
const legacyRobotId = resolveLegacyRobotId(startProperties)
|
||||
const roles = Array.isArray(startProperties.resourceRoles) && startProperties.resourceRoles.length
|
||||
? startProperties.resourceRoles
|
||||
: [createDefaultRobotRole(legacyRobotId)]
|
||||
|
||||
roles.forEach((role, index) => {
|
||||
role.roleKey ||= index === 0 ? DEFAULT_ROLE_KEY : createResourceKey('robot_role')
|
||||
role.resourceType = 'ROBOT'
|
||||
role.displayName ||= `机器人角色${index + 1}`
|
||||
role.required = role.required !== false
|
||||
role.quantity ||= { min: 1, max: 1 }
|
||||
role.deviceSlots = Array.isArray(role.deviceSlots) ? role.deviceSlots : []
|
||||
})
|
||||
|
||||
startProperties.schemaVersion = FLOW_SCHEMA_VERSION
|
||||
startProperties.resourceRoles = roles
|
||||
startProperties.inputParams = (startProperties.inputParams || []).filter((item) => item.name !== 'robotId')
|
||||
|
||||
const defaultRole = roles[0]
|
||||
const legacySlotByIdentity = new Map()
|
||||
roles.forEach((role) => role.deviceSlots.forEach((slot) => {
|
||||
const identity = `${role.roleKey}:${slot.deviceKind ?? 'any'}:${slot.legacyDeviceId || 'default'}`
|
||||
legacySlotByIdentity.set(identity, slot)
|
||||
}))
|
||||
|
||||
graph.nodes.forEach((node) => {
|
||||
if (!isRobotNode(node)) return
|
||||
const properties = node.properties || (node.properties = {})
|
||||
const target = properties.executionTarget || {
|
||||
roleKey: defaultRole.roleKey,
|
||||
dispatchPolicy: 'ONE',
|
||||
}
|
||||
target.roleKey ||= defaultRole.roleKey
|
||||
target.dispatchPolicy ||= 'ONE'
|
||||
target.deviceSlotKey ||= target.deviceBindings?.[0]?.slotKey || ''
|
||||
|
||||
const targetRole = roles.find((role) => role.roleKey === target.roleKey) || defaultRole
|
||||
const deviceParam = properties.nodeParams?.find((param) => param.name === 'deviceId')
|
||||
if (nodeRequiresDevice(node) && !target.deviceSlotKey) {
|
||||
const slot = findOrCreateLegacySlot(targetRole, node, legacySlotByIdentity, deviceParam?.input)
|
||||
if (slot) target.deviceSlotKey = slot.slotKey
|
||||
}
|
||||
delete target.deviceBindings
|
||||
properties.nodeParams = (properties.nodeParams || []).filter((param) => param.name !== 'deviceId')
|
||||
properties.executionTarget = target
|
||||
})
|
||||
|
||||
return graph
|
||||
}
|
||||
|
||||
export const ensureNodeExecutionResources = (sourceGraph, nodeId) => {
|
||||
const graph = normalizeFlowResourceSchema(sourceGraph)
|
||||
const node = graph.nodes.find((item) => item.id === nodeId)
|
||||
return { graph, node, start: getStartNode(graph) }
|
||||
}
|
||||
|
||||
export const validateFlowResources = (sourceGraph) => {
|
||||
const graph = normalizeFlowResourceSchema(sourceGraph)
|
||||
const roles = getResourceRoles(graph)
|
||||
const errors = []
|
||||
const roleKeys = new Set()
|
||||
|
||||
roles.forEach((role) => {
|
||||
if (!normalizedText(role.displayName)) errors.push({ nodeId: getStartNode(graph)?.id, message: '机器人角色名称不能为空' })
|
||||
if (roleKeys.has(role.roleKey)) errors.push({ nodeId: getStartNode(graph)?.id, message: `机器人角色标识重复:${role.roleKey}` })
|
||||
roleKeys.add(role.roleKey)
|
||||
const slotKeys = new Set()
|
||||
;(role.deviceSlots || []).forEach((slot) => {
|
||||
if (!normalizedText(slot.displayName)) errors.push({ nodeId: getStartNode(graph)?.id, message: `${role.displayName}存在未命名的设备用途` })
|
||||
if (slotKeys.has(slot.slotKey)) errors.push({ nodeId: getStartNode(graph)?.id, message: `${role.displayName}的设备用途标识重复` })
|
||||
slotKeys.add(slot.slotKey)
|
||||
})
|
||||
})
|
||||
|
||||
graph.nodes.filter(isRobotNode).forEach((node) => {
|
||||
const target = node.properties?.executionTarget
|
||||
const role = roles.find((item) => item.roleKey === target?.roleKey)
|
||||
if (!role) {
|
||||
errors.push({ nodeId: node.id, message: `${node.properties?.name || node.id}未选择有效的机器人角色` })
|
||||
return
|
||||
}
|
||||
if (target.deviceSlotKey && !(role.deviceSlots || []).some((slot) => slot.slotKey === target.deviceSlotKey)) {
|
||||
errors.push({ nodeId: node.id, message: `${node.properties?.name || node.id}引用的设备用途不存在` })
|
||||
}
|
||||
if (nodeRequiresDevice(node)
|
||||
&& !target.deviceSlotKey) {
|
||||
errors.push({ nodeId: node.id, message: `${node.properties?.name || node.id}未选择设备用途` })
|
||||
}
|
||||
})
|
||||
return { graph, errors }
|
||||
}
|
||||
|
||||
export const sanitizeFlowResourceDefinition = (sourceGraph) => {
|
||||
const graph = normalizeFlowResourceSchema(sourceGraph)
|
||||
const start = getStartNode(graph)
|
||||
;(start?.properties?.resourceRoles || []).forEach((role) => {
|
||||
delete role.legacyRobotId
|
||||
;(role.deviceSlots || []).forEach((slot) => delete slot.legacyDeviceId)
|
||||
})
|
||||
graph.nodes.filter(isRobotNode).forEach((node) => {
|
||||
node.properties.nodeParams = (node.properties?.nodeParams || []).filter((param) => param.name !== 'deviceId')
|
||||
})
|
||||
return graph
|
||||
}
|
||||
|
||||
export const mergeCopiedResourceRoles = (targetGraph, sourceRoles = [], copiedNodes = []) => {
|
||||
const graph = normalizeFlowResourceSchema(targetGraph)
|
||||
const start = getStartNode(graph)
|
||||
const targetRoles = start.properties.resourceRoles
|
||||
let addedRoles = 0
|
||||
let addedSlots = 0
|
||||
|
||||
copiedNodes.filter(isRobotNode).forEach((node) => {
|
||||
const target = node.properties?.executionTarget
|
||||
if (!target?.roleKey) return
|
||||
const sourceRole = sourceRoles.find((role) => role.roleKey === target.roleKey)
|
||||
let targetRole = targetRoles.find((role) => role.roleKey === target.roleKey)
|
||||
|| targetRoles.find((role) => sourceRole && role.displayName === sourceRole.displayName)
|
||||
|
||||
if (!targetRole && sourceRole) {
|
||||
targetRole = clone(sourceRole)
|
||||
delete targetRole.legacyRobotId
|
||||
;(targetRole.deviceSlots || []).forEach((slot) => delete slot.legacyDeviceId)
|
||||
targetRoles.push(targetRole)
|
||||
addedRoles += 1
|
||||
addedSlots += targetRole.deviceSlots?.length || 0
|
||||
}
|
||||
targetRole ||= targetRoles[0]
|
||||
target.roleKey = targetRole.roleKey
|
||||
|
||||
if (target.deviceSlotKey && !targetRole.deviceSlots?.some((slot) => slot.slotKey === target.deviceSlotKey)) {
|
||||
const sourceSlot = sourceRole?.deviceSlots?.find((slot) => slot.slotKey === target.deviceSlotKey)
|
||||
const sameSlot = targetRole.deviceSlots?.find((slot) => (
|
||||
sourceSlot && slot.displayName === sourceSlot.displayName && Number(slot.deviceKind) === Number(sourceSlot.deviceKind)
|
||||
))
|
||||
if (sameSlot) {
|
||||
target.deviceSlotKey = sameSlot.slotKey
|
||||
return
|
||||
}
|
||||
const newSlot = sourceSlot ? clone(sourceSlot) : createDeviceSlot(getDeviceKindForAction(node.properties?.action), targetRole.deviceSlots)
|
||||
delete newSlot.legacyDeviceId
|
||||
targetRole.deviceSlots.push(newSlot)
|
||||
target.deviceSlotKey = newSlot.slotKey
|
||||
addedSlots += 1
|
||||
}
|
||||
})
|
||||
|
||||
return { graph, start, addedRoles, addedSlots }
|
||||
}
|
||||
|
||||
export const getDeviceKindName = (deviceKind) => DEVICE_KIND_META[Number(deviceKind)]?.name || '任意设备'
|
||||
|
||||
export const mergeResourceRoleDefinitions = (roleGroups = []) => {
|
||||
const merged = []
|
||||
roleGroups.flat().forEach((sourceRole) => {
|
||||
let role = merged.find((item) => item.roleKey === sourceRole.roleKey)
|
||||
if (!role) {
|
||||
merged.push(clone(sourceRole))
|
||||
return
|
||||
}
|
||||
;(sourceRole.deviceSlots || []).forEach((sourceSlot) => {
|
||||
if (!role.deviceSlots.some((slot) => slot.slotKey === sourceSlot.slotKey)) {
|
||||
role.deviceSlots.push(clone(sourceSlot))
|
||||
}
|
||||
})
|
||||
})
|
||||
return merged
|
||||
}
|
||||
157
src/views/flow/components/RuntimeAssignmentsEditor.vue
Normal file
157
src/views/flow/components/RuntimeAssignmentsEditor.vue
Normal file
@ -0,0 +1,157 @@
|
||||
<template>
|
||||
<div class="runtime-assignments">
|
||||
<div v-for="role in roles" :key="role.roleKey" class="runtime-role">
|
||||
<div class="runtime-role__title">
|
||||
<span>{{ role.displayName }}</span>
|
||||
<el-tag v-if="role.required" size="small" type="info">必需</el-tag>
|
||||
</div>
|
||||
<el-form label-position="top">
|
||||
<el-form-item label="执行机器人" :required="role.required">
|
||||
<el-select
|
||||
:model-value="memberOf(role).robotId"
|
||||
filterable
|
||||
:loading="robotLoading"
|
||||
placeholder="请选择机器人"
|
||||
style="width: 100%"
|
||||
@change="(robotId) => changeRobot(role, robotId)"
|
||||
>
|
||||
<el-option v-for="item in robotOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-for="slot in role.deviceSlots" :key="slot.slotKey" :label="slot.displayName" :required="slot.required">
|
||||
<el-select
|
||||
v-model="memberOf(role).devices[slot.slotKey]"
|
||||
filterable
|
||||
:loading="loadingRoleKey === role.roleKey"
|
||||
:disabled="!memberOf(role).robotId"
|
||||
:placeholder="memberOf(role).robotId ? '请选择设备' : '请先选择机器人'"
|
||||
style="width: 100%"
|
||||
@change="emitValue"
|
||||
>
|
||||
<el-option v-for="item in optionsOf(role, slot)" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
<div v-if="memberOf(role).robotId && !optionsOf(role, slot).length" class="runtime-role__error">
|
||||
该机器人没有匹配的在线设备
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<el-empty v-if="!roles.length" :image-size="52" description="当前任务不需要机器人" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref, watch } from 'vue'
|
||||
import { getRobotList, getRobotDevicesByRobotId } from '@/api/inspection/robot'
|
||||
import { toDeviceOptions } from '@/utils/robotDevice'
|
||||
|
||||
const props = defineProps({
|
||||
roles: { type: Array, default: () => [] },
|
||||
modelValue: { type: Array, default: () => [] },
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const assignments = ref([])
|
||||
const robotOptions = ref([])
|
||||
const devicesByRole = reactive({})
|
||||
const robotLoading = ref(false)
|
||||
const loadingRoleKey = ref('')
|
||||
const clone = (value) => JSON.parse(JSON.stringify(value || []))
|
||||
|
||||
const assignmentOf = (role) => assignments.value.find((item) => item.roleKey === role.roleKey)
|
||||
const memberOf = (role) => assignmentOf(role).members[0]
|
||||
const emitValue = () => emit('update:modelValue', clone(assignments.value))
|
||||
|
||||
const loadRobots = async () => {
|
||||
robotLoading.value = true
|
||||
try {
|
||||
const response = await getRobotList({ pageNum: 1, pageSize: 1000 })
|
||||
robotOptions.value = (response.rows || []).map((robot) => ({
|
||||
label: `${robot.robotName || robot.robotId} (${robot.robotId})`,
|
||||
value: robot.robotId,
|
||||
}))
|
||||
} finally {
|
||||
robotLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const optionsOf = (role, slot) => toDeviceOptions((devicesByRole[role.roleKey] || []).filter((device) => (
|
||||
slot.deviceKind == null || Number(device.deviceKind) === Number(slot.deviceKind)
|
||||
)))
|
||||
|
||||
const loadDevices = async (role, preserve = true) => {
|
||||
const member = memberOf(role)
|
||||
if (!member.robotId) return
|
||||
loadingRoleKey.value = role.roleKey
|
||||
try {
|
||||
const response = await getRobotDevicesByRobotId(member.robotId)
|
||||
devicesByRole[role.roleKey] = Array.isArray(response.data) ? response.data : []
|
||||
role.deviceSlots.forEach((slot) => {
|
||||
const options = optionsOf(role, slot)
|
||||
if (!preserve || !options.some((option) => option.value === member.devices[slot.slotKey])) {
|
||||
member.devices[slot.slotKey] = options.length === 1 ? options[0].value : ''
|
||||
}
|
||||
})
|
||||
emitValue()
|
||||
} finally {
|
||||
loadingRoleKey.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
const changeRobot = async (role, robotId) => {
|
||||
const member = memberOf(role)
|
||||
member.robotId = robotId
|
||||
member.devices = Object.fromEntries(role.deviceSlots.map((slot) => [slot.slotKey, '']))
|
||||
await loadDevices(role, false)
|
||||
}
|
||||
|
||||
const initialize = async () => {
|
||||
const existing = clone(props.modelValue)
|
||||
assignments.value = props.roles.map((role) => existing.find((item) => item.roleKey === role.roleKey) || ({
|
||||
roleKey: role.roleKey,
|
||||
members: [{
|
||||
memberKey: `${role.roleKey}_1`,
|
||||
robotId: role.legacyRobotId || '',
|
||||
devices: Object.fromEntries((role.deviceSlots || []).map((slot) => [slot.slotKey, slot.legacyDeviceId || ''])),
|
||||
}],
|
||||
}))
|
||||
emitValue()
|
||||
await loadRobots()
|
||||
await Promise.all(props.roles.filter((role) => memberOf(role).robotId).map((role) => loadDevices(role)))
|
||||
}
|
||||
|
||||
const validate = () => {
|
||||
for (const role of props.roles) {
|
||||
const member = memberOf(role)
|
||||
if (role.required && !member.robotId) return `请为“${role.displayName}”选择机器人`
|
||||
for (const slot of role.deviceSlots || []) {
|
||||
if (slot.required && !member.devices[slot.slotKey]) return `请为“${role.displayName}”选择“${slot.displayName}”`
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
watch(() => props.roles, initialize, { immediate: true, deep: true })
|
||||
defineExpose({ validate })
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.runtime-role {
|
||||
padding: 12px 0 4px;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
.runtime-role__title {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.runtime-role__error {
|
||||
margin-top: 5px;
|
||||
color: var(--el-color-danger);
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
@ -1,328 +1,319 @@
|
||||
<!--
|
||||
* 文件说明:流程试运行抽屉,收集运行参数并绑定机器人设备。
|
||||
* 作用范围:仅服务于流程设计器模块。
|
||||
-->
|
||||
<!-- 流程试运行:分配机器人角色并收集业务运行参数。 -->
|
||||
<template>
|
||||
<el-drawer
|
||||
:model-value="props.drawer"
|
||||
title="试运行"
|
||||
:before-close="handleClose"
|
||||
>
|
||||
<el-form
|
||||
ref="ruleFormRef"
|
||||
style="max-width: 600px"
|
||||
:model="{ tableData }"
|
||||
:rules="rules"
|
||||
label-width="auto"
|
||||
>
|
||||
<div class="binding-panel" :class="{ 'binding-panel--warning': bindingSummary.missing > 0 }">
|
||||
<div class="binding-panel__title">
|
||||
<span class="binding-panel__dot"></span>
|
||||
机器人设备自动匹配
|
||||
</div>
|
||||
<div class="binding-panel__text" v-if="bindingLoading">正在读取机器人在线设备...</div>
|
||||
<div class="binding-panel__text" v-else-if="bindingSummary.robotId">
|
||||
已匹配 {{ bindingSummary.matched }} 个设备节点
|
||||
<span v-if="bindingSummary.missing">,{{ bindingSummary.missing }} 个节点未匹配并保留原值</span>
|
||||
<span v-if="bindingSummary.untouched">,{{ bindingSummary.untouched }} 个手动设备节点保持不变</span>
|
||||
</div>
|
||||
<div class="binding-panel__text" v-else>选择机器人后,将按节点类型自动填充该机器人上报的在线设备。</div>
|
||||
</div>
|
||||
<el-table
|
||||
:data="tableData"
|
||||
style="width: 100%; margin-bottom: 20px"
|
||||
row-key="name"
|
||||
default-expand-all
|
||||
>
|
||||
<el-table-column prop="name" label="入参名称">
|
||||
<template #default="{row}">
|
||||
<span>{{ row.name }}</span>
|
||||
<span v-if="row.required" style="color: #f56c6c; margin-left: 8px;">*</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="type" label="入参类型">
|
||||
<template #default="scope">
|
||||
<el-tag>{{ scope.row.type }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="value" label="入参值">
|
||||
<template #default="{ row, column, $index }">
|
||||
<el-form-item v-if="row.name === 'robotId'" label="" :prop="`tableData${row.propPath}value`" :rules="row.required ? [{ required: true, message: '请选择执行机器人', trigger: 'change' }] : []">
|
||||
<el-select v-model="row.value" filterable style="width: 100%" @change="handleRobotChange">
|
||||
<el-option
|
||||
v-for="item in robotIdOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-drawer :model-value="props.drawer" title="试运行" size="min(620px, 96vw)" :before-close="handleClose">
|
||||
<div class="section-heading">本次执行安排</div>
|
||||
<div class="assignment-tip">流程只保存角色分工,以下机器人和设备仅用于本次运行。</div>
|
||||
|
||||
<el-form-item v-else-if="row.type !== 'object' && !row.type.includes('array')" label="" :prop="`tableData${row.propPath}value`" :rules="row.required ? [{ required: true, message: '请输入变量名', trigger: 'blur' }] : []">
|
||||
<el-input v-if="row.type === 'string'" v-model="row.value" />
|
||||
<el-input-number v-if="row.type === 'number'" :controls="false" v-model="row.value" />
|
||||
<el-switch v-if="row.type === 'boolean'" v-model="row.value" />
|
||||
</el-form-item>
|
||||
<el-form-item v-else-if="row.type !== 'object' && row.type.includes('array')" label="" :prop="`tableData${row.propPath}value`" :rules="row.required ? [{ required: true, message: '请输入变量名', trigger: 'blur' }] : []">
|
||||
<el-input v-model="row.value" placeholder="请输入数组值,格式如:[1,2,3]" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div v-for="role in roles" :key="role.roleKey" class="assignment-row">
|
||||
<div class="assignment-row__title">
|
||||
<span>{{ role.displayName }}</span>
|
||||
<el-tag v-if="role.required" size="small" type="info">必需</el-tag>
|
||||
</div>
|
||||
<el-form label-position="top">
|
||||
<el-form-item label="执行机器人" required>
|
||||
<el-select
|
||||
:model-value="getMember(role).robotId"
|
||||
filterable
|
||||
:loading="robotLoading"
|
||||
placeholder="请选择机器人"
|
||||
style="width: 100%"
|
||||
@change="(robotId) => handleRobotChange(role, robotId)"
|
||||
>
|
||||
<el-option v-for="item in robotOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-for="slot in role.deviceSlots" :key="slot.slotKey" :label="slot.displayName" :required="slot.required">
|
||||
<el-select
|
||||
v-model="getMember(role).devices[slot.slotKey]"
|
||||
filterable
|
||||
:loading="loadingRoleKey === role.roleKey"
|
||||
:disabled="!getMember(role).robotId"
|
||||
:placeholder="getMember(role).robotId ? '请选择设备' : '请先选择机器人'"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in getSlotOptions(role, slot)"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
<div v-if="getMember(role).robotId && !getSlotOptions(role, slot).length" class="assignment-error">
|
||||
该机器人没有匹配的在线设备
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="section-heading section-heading--params">运行参数</div>
|
||||
<el-form ref="ruleFormRef" :model="{ tableData }" label-width="auto">
|
||||
<el-table :data="tableData" row-key="name" default-expand-all style="width: 100%; margin-bottom: 20px">
|
||||
<el-table-column prop="name" label="参数">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.name }}</span><span v-if="row.required" class="required-mark">*</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="type" label="类型" width="100">
|
||||
<template #default="{ row }"><el-tag>{{ row.type }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="value" label="值">
|
||||
<template #default="{ row }">
|
||||
<el-form-item
|
||||
v-if="row.type !== 'object' && !row.type.includes('array')"
|
||||
:prop="`tableData${row.propPath}value`"
|
||||
:rules="row.required ? [{ required: true, message: '请填写参数', trigger: 'blur' }] : []"
|
||||
>
|
||||
<el-input v-if="row.type === 'string'" v-model="row.value" />
|
||||
<el-input-number v-else-if="row.type === 'number'" v-model="row.value" :controls="false" />
|
||||
<el-switch v-else-if="row.type === 'boolean'" v-model="row.value" />
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-else-if="row.type !== 'object'"
|
||||
:prop="`tableData${row.propPath}value`"
|
||||
:rules="row.required ? [{ required: true, message: '请填写参数', trigger: 'blur' }] : []"
|
||||
>
|
||||
<el-input v-model="row.value" placeholder="数组格式,例如:[1,2,3]" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-empty v-if="!tableData.length" :image-size="52" description="没有需要填写的运行参数" />
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">关闭</el-button>
|
||||
<el-button type="primary" :loading="bindingLoading" :disabled="bindingLoading" @click="confirm">确定</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">关闭</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="confirm">开始运行</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { getStartNodeFormData, formatTableData } from '@/utils/flow'
|
||||
// import { Plus, Minus } from '@element-plus/icons-vue'
|
||||
import { reactive, ref, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { flowExecuteTrial } from '@/api/flow/flow'
|
||||
import { emitter } from '@/utils/eventBus';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { getRobotList, getRobotDevicesByRobotId } from '@/api/inspection/robot'
|
||||
import { getDeviceKindForAction, toDeviceOptions } from '@/utils/robotDevice'
|
||||
|
||||
const props = defineProps({
|
||||
drawer: Boolean,
|
||||
flowId: String
|
||||
})
|
||||
|
||||
const ruleFormRef = ref()
|
||||
const rules = ref({});
|
||||
const tableData = ref([])
|
||||
// 按流程缓存整套试运行参数,关闭或执行后再次打开时继续使用。
|
||||
const trialRunCache = new Map()
|
||||
const bindingLoading = ref(false)
|
||||
/**
|
||||
* 处理 emptyBindingSummary 对应的数据或交互;作用范围仅限当前组件或模块。
|
||||
* @param {*} robotId 机器人 ID
|
||||
*/
|
||||
const emptyBindingSummary = (robotId = '') => ({ robotId, matched: 0, missing: 0, untouched: 0 })
|
||||
const bindingSummary = ref(emptyBindingSummary())
|
||||
import { emitter } from '@/utils/eventBus'
|
||||
import { formatTableData, getStartNodeFormData } from '@/utils/flow'
|
||||
import { toDeviceOptions } from '@/utils/robotDevice'
|
||||
import {
|
||||
getResourceRoles,
|
||||
normalizeFlowResourceSchema,
|
||||
sanitizeFlowResourceDefinition,
|
||||
validateFlowResources,
|
||||
} from '@/utils/flowResources'
|
||||
|
||||
const props = defineProps({ drawer: Boolean, flowId: String })
|
||||
const emits = defineEmits(['close', 'changeState'])
|
||||
|
||||
/**
|
||||
* 克隆 cloneData 对应的数据或交互;作用范围仅限当前组件或模块。
|
||||
* @param {*} data 待处理的数据
|
||||
*/
|
||||
const cloneData = (data) => JSON.parse(JSON.stringify(data || []))
|
||||
const ruleFormRef = ref()
|
||||
const tableData = ref([])
|
||||
const roles = ref([])
|
||||
const runtimeAssignments = ref([])
|
||||
const robotOptions = ref([])
|
||||
const devicesByRole = reactive({})
|
||||
const robotLoading = ref(false)
|
||||
const loadingRoleKey = ref('')
|
||||
const submitting = ref(false)
|
||||
const trialRunCache = new Map()
|
||||
|
||||
// 根据参数层级恢复缓存值,同时兼容流程参数结构发生增删的情况。
|
||||
/**
|
||||
* 合并 mergeCachedValues 对应的数据或交互;作用范围仅限当前组件或模块。
|
||||
* @param {*} source 源数据
|
||||
* @param {*} cached 缓存数据
|
||||
*/
|
||||
const mergeCachedValues = (source, cached) => {
|
||||
const cachedByName = new Map((cached || []).map(item => [item.name, item]))
|
||||
return (source || []).map((item, index) => {
|
||||
const cachedAtIndex = cached?.[index]
|
||||
const cachedItem = cachedAtIndex?.name === item.name ? cachedAtIndex : cachedByName.get(item.name)
|
||||
if (!cachedItem) return item
|
||||
const merged = { ...item, value: cachedItem.value }
|
||||
if (Array.isArray(item.children)) {
|
||||
merged.children = mergeCachedValues(item.children, cachedItem.children)
|
||||
}
|
||||
return merged
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 cacheKey 对应的数据或交互;作用范围仅限当前组件或模块。
|
||||
*/
|
||||
const clone = (value) => JSON.parse(JSON.stringify(value || []))
|
||||
const cacheKey = () => props.flowId || '__new_flow__'
|
||||
|
||||
// 深拷贝快照,避免后续表单编辑直接修改缓存内容。
|
||||
/**
|
||||
* 缓存 rememberTrialRunParams 对应的数据或交互;作用范围仅限当前组件或模块。
|
||||
*/
|
||||
const rememberTrialRunParams = () => {
|
||||
if (!tableData.value.length) return
|
||||
trialRunCache.set(cacheKey(), cloneData(tableData.value))
|
||||
const mergeCachedValues = (source, cached) => {
|
||||
const cachedByName = new Map((cached || []).map((item) => [item.name, item]))
|
||||
return (source || []).map((item, index) => {
|
||||
const cachedItem = cached?.[index]?.name === item.name ? cached[index] : cachedByName.get(item.name)
|
||||
if (!cachedItem) return item
|
||||
const merged = { ...item, value: cachedItem.value }
|
||||
if (Array.isArray(item.children)) merged.children = mergeCachedValues(item.children, cachedItem.children)
|
||||
return merged
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 handleClose 对应的数据或交互;作用范围仅限当前组件或模块。
|
||||
*/
|
||||
const handleClose = () => {
|
||||
rememberTrialRunParams()
|
||||
ruleFormRef.value?.clearValidate()
|
||||
emits('close')
|
||||
const getAssignment = (role) => runtimeAssignments.value.find((item) => item.roleKey === role.roleKey)
|
||||
const getMember = (role) => getAssignment(role).members[0]
|
||||
|
||||
const createAssignments = (cached = []) => roles.value.map((role) => {
|
||||
const old = cached.find((item) => item.roleKey === role.roleKey)
|
||||
return old || {
|
||||
roleKey: role.roleKey,
|
||||
members: [{
|
||||
memberKey: `${role.roleKey}_1`,
|
||||
robotId: role.legacyRobotId || '',
|
||||
devices: Object.fromEntries((role.deviceSlots || []).map((slot) => [slot.slotKey, slot.legacyDeviceId || ''])),
|
||||
}],
|
||||
}
|
||||
})
|
||||
|
||||
const remember = () => {
|
||||
trialRunCache.set(cacheKey(), {
|
||||
tableData: clone(tableData.value),
|
||||
runtimeAssignments: clone(runtimeAssignments.value),
|
||||
})
|
||||
}
|
||||
|
||||
watch(() => props.drawer,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
const data = getStartNodeFormData()
|
||||
const cached = trialRunCache.get(cacheKey())
|
||||
tableData.value = mergeCachedValues(data, cached)
|
||||
const robotId = tableData.value.find(item => item.name === 'robotId')?.value || ''
|
||||
bindingSummary.value = emptyBindingSummary(robotId)
|
||||
loadRobotOptions()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* 确认 confirm 对应的数据或交互;作用范围仅限当前组件或模块。
|
||||
*/
|
||||
const confirm = () => {
|
||||
rememberTrialRunParams()
|
||||
lf.fitView()
|
||||
ruleFormRef.value.validate().then(async () => {
|
||||
const formData = formatTableData(tableData.value)
|
||||
const { nodes } = lf.getGraphData()
|
||||
nodes.forEach(item => {
|
||||
emitter.emit('contentChange', { id: item.id})
|
||||
})
|
||||
|
||||
const flowData = JSON.stringify(lf.getGraphData())
|
||||
const { robotId, ...runParams } = formData
|
||||
const res = await flowExecuteTrial({
|
||||
flowData,
|
||||
itemId: props.flowId,
|
||||
robotId,
|
||||
runParams
|
||||
})
|
||||
if (res.code === 200) {
|
||||
emits('changeState', {
|
||||
type: 'testRunning',
|
||||
instId: res.msg
|
||||
})
|
||||
} else {
|
||||
ElMessage.error(res.msg)
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
const robotIdOptions = ref([])
|
||||
|
||||
/**
|
||||
* 处理 handleRobotChange 对应的数据或交互;作用范围仅限当前组件或模块。
|
||||
* @param {*} robotId 机器人 ID
|
||||
*/
|
||||
const handleRobotChange = async (robotId) => {
|
||||
// 先拉取机器人设备,再按每个设备节点声明的设备类型筛选候选项。
|
||||
// 仅在候选唯一时自动回填;无候选或多候选均交由用户确认,避免绑定错误设备。
|
||||
bindingSummary.value = emptyBindingSummary(robotId || '')
|
||||
if (!robotId) return
|
||||
bindingLoading.value = true
|
||||
try {
|
||||
const response = await getRobotDevicesByRobotId(robotId)
|
||||
const devices = Array.isArray(response.data) ? response.data : []
|
||||
let matched = 0
|
||||
let missing = 0
|
||||
let untouched = 0
|
||||
const missingNodes = []
|
||||
const { nodes } = lf.getGraphData()
|
||||
|
||||
nodes.forEach((node) => {
|
||||
if (node.properties?.nodeType !== 'EDGE') return
|
||||
const nodeParams = JSON.parse(JSON.stringify(node.properties.nodeParams || []))
|
||||
const robotParam = nodeParams.find(param => param.name === 'robotId')
|
||||
if (robotParam) robotParam.input = robotId
|
||||
|
||||
const deviceParam = nodeParams.find(param => param.name === 'deviceId')
|
||||
if (!deviceParam) {
|
||||
if (robotParam) lf.setProperties(node.id, { ...node.properties, nodeParams })
|
||||
return
|
||||
}
|
||||
|
||||
const kind = getDeviceKindForAction(node.properties.action)
|
||||
if (!kind) {
|
||||
untouched += 1
|
||||
if (robotParam) lf.setProperties(node.id, { ...node.properties, nodeParams })
|
||||
return
|
||||
}
|
||||
|
||||
const candidates = devices.filter(device => Number(device.deviceKind) === kind)
|
||||
if (!candidates.length) {
|
||||
missing += 1
|
||||
missingNodes.push(node.properties.name || node.id)
|
||||
if (robotParam) lf.setProperties(node.id, { ...node.properties, nodeParams })
|
||||
return
|
||||
}
|
||||
|
||||
const currentCandidate = candidates.find(device => device.deviceId === deviceParam.input)
|
||||
deviceParam.input = (currentCandidate || candidates[0]).deviceId
|
||||
deviceParam.componentType = 'select'
|
||||
deviceParam.selectOptions = toDeviceOptions(candidates)
|
||||
deviceParam.deviceLocked = candidates.length === 1
|
||||
lf.setProperties(node.id, { ...node.properties, nodeParams })
|
||||
matched += 1
|
||||
})
|
||||
|
||||
bindingSummary.value = { robotId, matched, missing, untouched }
|
||||
if (missingNodes.length) {
|
||||
ElMessage.warning(`以下节点没有匹配的在线设备,已保留原值:${missingNodes.join('、')}`)
|
||||
} else if (matched > 0) {
|
||||
ElMessage.success(`已自动匹配 ${matched} 个设备节点`)
|
||||
}
|
||||
} catch (error) {
|
||||
bindingSummary.value = emptyBindingSummary(robotId)
|
||||
console.error('自动匹配机器人设备失败:', error)
|
||||
ElMessage.error('读取机器人设备失败')
|
||||
} finally {
|
||||
bindingLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取在线机器人列表 */
|
||||
const loadRobotOptions = async () => {
|
||||
// 试运行不需要要有连接状态为连接的,因为可以选择未连接的机器人进行试运行来保存流程数据
|
||||
// , connectStatus: '1'
|
||||
const res = await getRobotList({ pageNum: 1, pageSize: 1000 })
|
||||
if (res.code === 200) {
|
||||
robotIdOptions.value = res.rows?.map((item) => {
|
||||
return {
|
||||
label: `${item.robotName || item.robotId} (${item.robotId})`,
|
||||
value: item.robotId
|
||||
}
|
||||
})
|
||||
}
|
||||
robotLoading.value = true
|
||||
try {
|
||||
const response = await getRobotList({ pageNum: 1, pageSize: 1000 })
|
||||
robotOptions.value = (response.rows || []).map((robot) => ({
|
||||
label: `${robot.robotName || robot.robotId} (${robot.robotId})`,
|
||||
value: robot.robotId,
|
||||
}))
|
||||
} finally {
|
||||
robotLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const getSlotOptions = (role, slot) => toDeviceOptions((devicesByRole[role.roleKey] || []).filter((device) => (
|
||||
slot.deviceKind == null || Number(device.deviceKind) === Number(slot.deviceKind)
|
||||
)))
|
||||
|
||||
const loadRoleDevices = async (role, preserveValues = true) => {
|
||||
const member = getMember(role)
|
||||
if (!member.robotId) {
|
||||
devicesByRole[role.roleKey] = []
|
||||
return
|
||||
}
|
||||
loadingRoleKey.value = role.roleKey
|
||||
try {
|
||||
const response = await getRobotDevicesByRobotId(member.robotId)
|
||||
devicesByRole[role.roleKey] = Array.isArray(response.data) ? response.data : []
|
||||
role.deviceSlots.forEach((slot) => {
|
||||
const options = getSlotOptions(role, slot)
|
||||
const currentValid = options.some((option) => option.value === member.devices[slot.slotKey])
|
||||
if (!preserveValues || !currentValid) member.devices[slot.slotKey] = options.length === 1 ? options[0].value : ''
|
||||
})
|
||||
} catch (error) {
|
||||
devicesByRole[role.roleKey] = []
|
||||
ElMessage.error(`读取“${role.displayName}”的设备失败`)
|
||||
} finally {
|
||||
loadingRoleKey.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
const handleRobotChange = async (role, robotId) => {
|
||||
const member = getMember(role)
|
||||
member.robotId = robotId
|
||||
member.devices = Object.fromEntries(role.deviceSlots.map((slot) => [slot.slotKey, '']))
|
||||
await loadRoleDevices(role, false)
|
||||
}
|
||||
|
||||
const initialize = async () => {
|
||||
const graph = normalizeFlowResourceSchema(globalThis.lf.getGraphData())
|
||||
roles.value = getResourceRoles(graph)
|
||||
const cached = trialRunCache.get(cacheKey())
|
||||
tableData.value = mergeCachedValues(getStartNodeFormData(), cached?.tableData)
|
||||
runtimeAssignments.value = createAssignments(cached?.runtimeAssignments)
|
||||
await loadRobotOptions()
|
||||
await Promise.all(roles.value.filter((role) => getMember(role).robotId).map((role) => loadRoleDevices(role)))
|
||||
}
|
||||
|
||||
watch(() => props.drawer, (open) => { if (open) initialize() })
|
||||
|
||||
const handleClose = () => {
|
||||
remember()
|
||||
ruleFormRef.value?.clearValidate()
|
||||
emits('close')
|
||||
}
|
||||
|
||||
const validateAssignments = () => {
|
||||
for (const role of roles.value) {
|
||||
const member = getMember(role)
|
||||
if (role.required && !member.robotId) return `请为“${role.displayName}”选择机器人`
|
||||
for (const slot of role.deviceSlots || []) {
|
||||
if (slot.required && !member.devices[slot.slotKey]) return `请为“${role.displayName}”选择“${slot.displayName}”`
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
const confirm = async () => {
|
||||
const currentGraph = globalThis.lf.getGraphData()
|
||||
const resourceValidation = validateFlowResources(currentGraph)
|
||||
if (resourceValidation.errors.length) {
|
||||
ElMessage.warning(resourceValidation.errors[0].message)
|
||||
return
|
||||
}
|
||||
const assignmentError = validateAssignments()
|
||||
if (assignmentError) {
|
||||
ElMessage.warning(assignmentError)
|
||||
return
|
||||
}
|
||||
const formValid = await ruleFormRef.value?.validate().catch(() => false)
|
||||
if (formValid === false) return
|
||||
|
||||
remember()
|
||||
submitting.value = true
|
||||
try {
|
||||
const formData = formatTableData(tableData.value)
|
||||
resourceValidation.graph.nodes.forEach((node) => emitter.emit('contentChange', { id: node.id }))
|
||||
const executionDefinition = sanitizeFlowResourceDefinition(globalThis.lf.getGraphData())
|
||||
const firstRobotId = runtimeAssignments.value.flatMap((item) => item.members).find((member) => member.robotId)?.robotId || ''
|
||||
const response = await flowExecuteTrial({
|
||||
flowData: JSON.stringify(executionDefinition),
|
||||
itemId: props.flowId,
|
||||
robotId: firstRobotId,
|
||||
runtimeAssignments: runtimeAssignments.value,
|
||||
runParams: formData,
|
||||
})
|
||||
if (response.code !== 200) {
|
||||
ElMessage.error(response.msg)
|
||||
return
|
||||
}
|
||||
emits('changeState', { type: 'testRunning', instId: response.msg })
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.binding-panel {
|
||||
margin-bottom: 16px;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid #d7e5f7;
|
||||
border-radius: 6px;
|
||||
background: #f5f9ff;
|
||||
<style scoped lang="scss">
|
||||
.section-heading {
|
||||
margin: 6px 0 10px;
|
||||
color: var(--el-text-color-primary);
|
||||
font-size: 15px;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.binding-panel--warning {
|
||||
border-color: #f3d19e;
|
||||
background: #fdf6ec;
|
||||
.section-heading--params {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.binding-panel__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #24364b;
|
||||
font-weight: 600;
|
||||
.assignment-tip {
|
||||
margin-bottom: 14px;
|
||||
padding: 10px 12px;
|
||||
border-left: 3px solid var(--el-color-primary);
|
||||
background: var(--el-fill-color-light);
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.binding-panel__dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #409eff;
|
||||
.assignment-row {
|
||||
padding: 14px 0 4px;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
.binding-panel__text {
|
||||
margin-top: 6px;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
.assignment-row__title {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.assignment-error,
|
||||
.required-mark {
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
|
||||
.assignment-error {
|
||||
margin-top: 5px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.required-mark {
|
||||
margin-left: 6px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -3,6 +3,11 @@
|
||||
* 作用范围:仅服务于流程设计器模块。
|
||||
-->
|
||||
<template>
|
||||
<RobotExecutionTarget
|
||||
v-if="isRobotNode"
|
||||
:data="props.data"
|
||||
v-model="formData.executionTarget"
|
||||
/>
|
||||
<el-collapse v-model="activeNames">
|
||||
<!-- 输入参数 -->
|
||||
<el-collapse-item name="1" icon-position="left">
|
||||
@ -15,7 +20,7 @@
|
||||
</el-button>
|
||||
</template>
|
||||
<div class="form__container">
|
||||
<el-button type="primary" v-if="hasBindableDevice" size="small" @click="openRobotForm(props.data.properties.action)">{{ robotActionButtonText }}</el-button>
|
||||
<el-button type="primary" v-if="hasDeviceDebugAction" size="small" @click="openRobotForm(props.data.properties.action)">{{ robotActionButtonText }}</el-button>
|
||||
<el-form :inline="true" :model="formData" :rules="rules" ref="dynamicFormRef" label-position="top"
|
||||
label-width="auto">
|
||||
<div v-for="(property, index) in formData.nodeParams" :key="index">
|
||||
@ -118,12 +123,14 @@
|
||||
import { computed, ref, reactive, watch, nextTick } from 'vue'
|
||||
import { Plus, Minus } from '@element-plus/icons-vue'
|
||||
import FormItemRecursive from '../FormItemRecursive.vue'
|
||||
import RobotExecutionTarget from './RobotExecutionTarget.vue'
|
||||
import { getInput } from '@/utils/flow'
|
||||
import { useQuote } from './useQuote.js'
|
||||
import { getArmStatus, getArmJointState, getAgvStatus, getAgvStations } from '@/api/flow/flow'
|
||||
import { getRobotList, getRobotDevicesByRobotId } from '@/api/inspection/robot'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getDeviceKindForAction, toDeviceOptions } from '@/utils/robotDevice'
|
||||
import { actionRequiresDevice } from '@/utils/flowResources'
|
||||
|
||||
const props = defineProps({
|
||||
data: Object
|
||||
@ -133,7 +140,8 @@ const emit = defineEmits(['save-success', 'save-error'])
|
||||
|
||||
const formData = reactive({
|
||||
nodeParams: [],
|
||||
outputParams: []
|
||||
outputParams: [],
|
||||
executionTarget: {}
|
||||
})
|
||||
|
||||
const rules = reactive({})
|
||||
@ -197,6 +205,7 @@ const initData = () => {
|
||||
const outputParams = JSON.parse(JSON.stringify(props.data.properties.outputParams || []))
|
||||
formData.nodeParams = nodeParams;
|
||||
formData.outputParams = outputParams;
|
||||
formData.executionTarget = JSON.parse(JSON.stringify(props.data.properties.executionTarget || {}));
|
||||
}
|
||||
|
||||
watch(
|
||||
@ -220,6 +229,13 @@ const validateAndSave = async () => {
|
||||
if (outputFormRef.value) {
|
||||
await outputFormRef.value.validate((valid) => { if (!valid) outputValid = false })
|
||||
}
|
||||
if (isRobotNode.value && !formData.executionTarget.roleKey) inputValid = false
|
||||
if (isRobotNode.value
|
||||
&& actionRequiresDevice(props.data.properties.action)
|
||||
&& !formData.executionTarget.deviceSlotKey) {
|
||||
inputValid = false
|
||||
ElMessage.warning('请选择该节点使用的设备用途')
|
||||
}
|
||||
|
||||
// 保存到 lf
|
||||
lf.setProperties(props.data.id, {
|
||||
@ -290,10 +306,9 @@ const testRule = (property) => {
|
||||
return [
|
||||
{ required: true, message: '请从站点列表中选择站点', trigger: 'change' },
|
||||
{ validator: () => {
|
||||
const deviceId = formData.nodeParams.find(param => param.name === 'deviceId')?.input
|
||||
const selectedFromList = property.selectOptions?.some(option => option.value === property.input)
|
||||
if (selectedFromList && property.stationDeviceId === deviceId) return Promise.resolve()
|
||||
return Promise.reject(new Error('设备已变化,请重新加载站点列表'))
|
||||
if (selectedFromList) return Promise.resolve()
|
||||
return Promise.reject(new Error('请重新加载并选择站点'))
|
||||
}, trigger: 'change' }
|
||||
]
|
||||
}
|
||||
@ -331,10 +346,11 @@ const testRule = (property) => {
|
||||
const robotFormAction = ref()
|
||||
const stationListLoading = ref(false)
|
||||
const POSITION_ACTIONS = ['AGV_MOVE_TO_POINT', 'ARM_MOVE_TO_POINT', 'ARM_MOVE_TO_J']
|
||||
const hasBindableDevice = computed(() =>
|
||||
formData.nodeParams.some(param => param.name === 'deviceId')
|
||||
&& Boolean(getDeviceKindForAction(props.data.properties.action))
|
||||
)
|
||||
const isRobotNode = computed(() => props.data.properties?.nodeType === 'EDGE')
|
||||
const hasDeviceDebugAction = computed(() => (
|
||||
POSITION_ACTIONS.includes(props.data.properties.action)
|
||||
|| props.data.properties.action === 'AGV_MOVE_TO_STATION'
|
||||
))
|
||||
const robotActionButtonText = computed(() => {
|
||||
if (props.data.properties.action === 'AGV_MOVE_TO_STATION') return '选择机器人并加载站点'
|
||||
if (POSITION_ACTIONS.includes(props.data.properties.action)) return '选择机器人并获取位置'
|
||||
@ -361,8 +377,7 @@ const submitRobotForm = () => {
|
||||
const testMap = {
|
||||
x: x,
|
||||
y: y,
|
||||
theta: theta,
|
||||
deviceId: robotForm.deviceId
|
||||
theta: theta
|
||||
}
|
||||
// 将获取到的位置信息设置到 formData 中
|
||||
formData.nodeParams.forEach(param => {
|
||||
@ -390,15 +405,12 @@ const submitRobotForm = () => {
|
||||
throw new Error('AGV未返回可用站点')
|
||||
}
|
||||
const stationParam = formData.nodeParams.find(param => param.name === 'stationId')
|
||||
const deviceParam = formData.nodeParams.find(param => param.name === 'deviceId')
|
||||
if (stationParam) {
|
||||
stationParam.selectOptions = stationOptions
|
||||
stationParam.stationDeviceId = robotForm.deviceId
|
||||
if (!stationOptions.some(item => item.value === stationParam.input)) {
|
||||
stationParam.input = ''
|
||||
}
|
||||
}
|
||||
if (deviceParam) deviceParam.input = robotForm.deviceId
|
||||
dialogVisible.value = false
|
||||
ElMessage.success(`已加载${stationOptions.length}个AGV站点`)
|
||||
} catch (error) {
|
||||
@ -424,7 +436,6 @@ const submitRobotForm = () => {
|
||||
rx: rx,
|
||||
ry: ry,
|
||||
rz: rz,
|
||||
deviceId: robotForm.deviceId
|
||||
}
|
||||
// 将获取到的位置信息设置到 formData 中
|
||||
formData.nodeParams.forEach(param => {
|
||||
@ -453,28 +464,13 @@ const submitRobotForm = () => {
|
||||
}
|
||||
dialogVisible.value = false
|
||||
const targetParam = formData.nodeParams.find(param => param.name === 'target')
|
||||
const deviceParam = formData.nodeParams.find(param => param.name === 'deviceId')
|
||||
if (targetParam) targetParam.input = JSON.stringify(target)
|
||||
if (deviceParam) deviceParam.input = robotForm.deviceId
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取机械臂关节位置失败:', error)
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
robotFormRef.value.validate((valid) => {
|
||||
if (!valid) return
|
||||
const deviceParam = formData.nodeParams.find(param => param.name === 'deviceId')
|
||||
if (deviceParam) {
|
||||
deviceParam.input = robotForm.deviceId
|
||||
deviceParam.componentType = 'select'
|
||||
deviceParam.selectOptions = [...deviceOptions.value]
|
||||
deviceParam.deviceLocked = deviceOptions.value.length === 1
|
||||
}
|
||||
dialogVisible.value = false
|
||||
ElMessage.success('机器人设备已匹配')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -500,8 +496,7 @@ const toStationOptions = (stations) => (Array.isArray(stations) ? stations : [])
|
||||
const openRobotForm = async (action) => {
|
||||
dialogVisible.value = true
|
||||
robotFormAction.value = action
|
||||
const currentDeviceId = formData.nodeParams.find(param => param.name === 'deviceId')?.input || ''
|
||||
robotForm.deviceId = currentDeviceId
|
||||
robotForm.deviceId = ''
|
||||
await loadRobotOptions()
|
||||
if (robotForm.robotId) await loadRobotDevices(robotForm.robotId)
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
* 作用范围:仅服务于流程设计器模块。
|
||||
-->
|
||||
<template>
|
||||
<RobotExecutionTarget :data="props.data" v-model="formData.executionTarget" />
|
||||
<el-collapse v-model="activeNames">
|
||||
<el-collapse v-model="activeNames">
|
||||
<el-collapse-item name="1" icon-position="left">
|
||||
@ -20,15 +21,6 @@
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<div class="form__container">
|
||||
<div class="device-binding-toolbar">
|
||||
<div>
|
||||
<div class="device-binding-toolbar__title">执行设备</div>
|
||||
<div class="device-binding-toolbar__desc">从机器人当前上报的全部在线设备中选择</div>
|
||||
</div>
|
||||
<el-button type="primary" plain size="small" @click="openDeviceDialog">
|
||||
选择机器人设备
|
||||
</el-button>
|
||||
</div>
|
||||
<el-form
|
||||
:inline="true"
|
||||
:model="formData"
|
||||
@ -203,51 +195,16 @@
|
||||
</el-collapse>
|
||||
</el-collapse>
|
||||
|
||||
<el-dialog v-model="deviceDialogVisible" title="选择通用指令执行设备" width="520" append-to-body>
|
||||
<div class="device-dialog-tip">通用指令不限制设备类型,设备列表来自机器人最近一次 QUIC 在线上报。</div>
|
||||
<el-form ref="deviceFormRef" :model="deviceForm" :rules="deviceFormRules" label-width="86px">
|
||||
<el-form-item label="机器人" prop="robotId">
|
||||
<el-select
|
||||
v-model="deviceForm.robotId"
|
||||
filterable
|
||||
:loading="robotLoading"
|
||||
placeholder="请选择在线机器人"
|
||||
style="width: 100%"
|
||||
@change="loadRobotDevices"
|
||||
>
|
||||
<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="执行设备" prop="deviceId">
|
||||
<el-select
|
||||
v-model="deviceForm.deviceId"
|
||||
filterable
|
||||
:loading="deviceLoading"
|
||||
:disabled="!deviceForm.robotId || deviceOptions.length === 1"
|
||||
placeholder="请选择该机器人上的设备"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option v-for="item in deviceOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
<div v-if="deviceOptions.length === 1" class="device-auto-hint">仅有一个在线设备,已自动选择并锁定</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="deviceDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="confirmDevice">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, watch, nextTick, onBeforeUnmount, onMounted } from 'vue'
|
||||
import { Plus, Minus } from '@element-plus/icons-vue'
|
||||
import FormItemRecursive from '../FormItemRecursive.vue'
|
||||
import RobotExecutionTarget from './RobotExecutionTarget.vue'
|
||||
import * as monaco from 'monaco-editor';
|
||||
import { getInput } from '@/utils/flow'
|
||||
import { useQuote } from './useQuote.js'
|
||||
import { getRobotList, getRobotDevicesByRobotId } from '@/api/inspection/robot'
|
||||
import { toDeviceOptions } from '@/utils/robotDevice'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const props = defineProps({
|
||||
@ -258,7 +215,8 @@ const emit = defineEmits(['save-success', 'save-error'])
|
||||
|
||||
const formData = reactive({
|
||||
nodeParams: [],
|
||||
outputParams: []
|
||||
outputParams: [],
|
||||
executionTarget: {}
|
||||
})
|
||||
|
||||
const codeRef = ref()
|
||||
@ -347,83 +305,6 @@ const dynamicFormRef = ref()
|
||||
const outputFormRef = ref()
|
||||
const activeNames = ref(['1', '2'])
|
||||
|
||||
const deviceDialogVisible = ref(false)
|
||||
const deviceFormRef = ref()
|
||||
const robotLoading = ref(false)
|
||||
const deviceLoading = ref(false)
|
||||
const robotOptions = ref([])
|
||||
const deviceOptions = ref([])
|
||||
const deviceForm = reactive({ robotId: '', deviceId: '' })
|
||||
const deviceFormRules = {
|
||||
robotId: [{ required: true, message: '请选择机器人', trigger: 'change' }],
|
||||
deviceId: [{ required: true, message: '请选择执行设备', trigger: 'change' }]
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载 loadRobotOptions 对应的数据或交互;作用范围仅限当前组件或模块。
|
||||
*/
|
||||
const loadRobotOptions = async () => {
|
||||
robotLoading.value = true
|
||||
try {
|
||||
const response = await getRobotList({ pageNum: 1, pageSize: 1000, connectStatus: '1' })
|
||||
robotOptions.value = (response.rows || []).map(robot => ({
|
||||
value: robot.robotId,
|
||||
label: `${robot.robotName || robot.robotId} (${robot.robotId})`
|
||||
}))
|
||||
} finally {
|
||||
robotLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载 loadRobotDevices 对应的数据或交互;作用范围仅限当前组件或模块。
|
||||
* @param {*} robotId 机器人 ID
|
||||
*/
|
||||
const loadRobotDevices = async (robotId) => {
|
||||
deviceForm.deviceId = ''
|
||||
deviceOptions.value = []
|
||||
if (!robotId) return
|
||||
deviceLoading.value = true
|
||||
try {
|
||||
const response = await getRobotDevicesByRobotId(robotId)
|
||||
const devices = Array.isArray(response.data) ? response.data : []
|
||||
deviceOptions.value = toDeviceOptions(devices)
|
||||
if (devices.length) deviceForm.deviceId = devices[0].deviceId
|
||||
if (!devices.length) ElMessage.warning('当前机器人没有在线设备')
|
||||
} finally {
|
||||
deviceLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开 openDeviceDialog 对应的数据或交互;作用范围仅限当前组件或模块。
|
||||
*/
|
||||
const openDeviceDialog = async () => {
|
||||
deviceDialogVisible.value = true
|
||||
deviceForm.robotId = ''
|
||||
deviceForm.deviceId = ''
|
||||
deviceOptions.value = []
|
||||
await loadRobotOptions()
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认 confirmDevice 对应的数据或交互;作用范围仅限当前组件或模块。
|
||||
*/
|
||||
const confirmDevice = async () => {
|
||||
const valid = await deviceFormRef.value?.validate().catch(() => false)
|
||||
if (!valid) return
|
||||
const deviceParam = formData.nodeParams.find(item => item.name === 'deviceId')
|
||||
if (!deviceParam) return
|
||||
deviceParam.type = 'input'
|
||||
deviceParam.input = deviceForm.deviceId
|
||||
deviceParam.componentType = 'select'
|
||||
deviceParam.selectOptions = [...deviceOptions.value]
|
||||
deviceParam.required = true
|
||||
deviceParam.deviceLocked = deviceOptions.value.length === 1
|
||||
deviceDialogVisible.value = false
|
||||
ElMessage.success('执行设备已选择')
|
||||
}
|
||||
|
||||
// 使用共享的 quote 逻辑
|
||||
const { quoteOptions, cascaderRefs, handleTypeChange, cascaderChange, visibleChange } = useQuote(props.data.id)
|
||||
|
||||
@ -479,6 +360,7 @@ const initData = () => {
|
||||
const outputParams = JSON.parse(JSON.stringify(props.data.properties.outputParams || []))
|
||||
formData.nodeParams = nodeParams;
|
||||
formData.outputParams = outputParams;
|
||||
formData.executionTarget = JSON.parse(JSON.stringify(props.data.properties.executionTarget || {}));
|
||||
const codeParams = props.data.properties.nodeParams.find(item => item.name === 'requestJson')
|
||||
if (codeParams) {
|
||||
setTimeout(() => {
|
||||
@ -512,6 +394,10 @@ const validateAndSave = async () => {
|
||||
if (outputFormRef.value) {
|
||||
await outputFormRef.value.validate((valid) => { if (!valid) outputValid = false })
|
||||
}
|
||||
if (!formData.executionTarget.roleKey || !formData.executionTarget.deviceSlotKey) {
|
||||
inputValid = false
|
||||
ElMessage.warning('请选择执行角色和设备用途')
|
||||
}
|
||||
|
||||
// 保存到 lf
|
||||
lf.setProperties(props.data.id, {
|
||||
@ -616,45 +502,6 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.device-binding-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 14px;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid #d9e5f4;
|
||||
border-radius: 6px;
|
||||
background: #f6f9fd;
|
||||
}
|
||||
|
||||
.device-binding-toolbar__title {
|
||||
color: #26364a;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.device-binding-toolbar__desc,
|
||||
.device-dialog-tip,
|
||||
.device-auto-hint {
|
||||
color: #6b7a90;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.device-dialog-tip {
|
||||
margin-bottom: 16px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 6px;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.device-auto-hint {
|
||||
width: 100%;
|
||||
margin-top: 4px;
|
||||
color: var(--el-color-success);
|
||||
}
|
||||
|
||||
.code__container {
|
||||
display: flex;
|
||||
position: relative;
|
||||
|
||||
117
src/views/flow/components/params/RobotExecutionTarget.vue
Normal file
117
src/views/flow/components/params/RobotExecutionTarget.vue
Normal file
@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<div class="execution-target">
|
||||
<div class="execution-target__heading">执行设置</div>
|
||||
<el-form label-position="top">
|
||||
<el-form-item label="由谁执行" required>
|
||||
<el-select v-model="localTarget.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 v-if="hasDeviceParameter" label="使用设备" required>
|
||||
<el-select
|
||||
v-model="deviceSlotKey"
|
||||
style="width: 100%"
|
||||
:placeholder="matchingSlots.length ? '请选择设备用途' : '请先在开始节点配置设备用途'"
|
||||
@change="emitTarget"
|
||||
>
|
||||
<el-option
|
||||
v-for="slot in matchingSlots"
|
||||
:key="slot.slotKey"
|
||||
:label="`${slot.displayName} · ${getDeviceKindName(slot.deviceKind)}`"
|
||||
:value="slot.slotKey"
|
||||
/>
|
||||
</el-select>
|
||||
<div v-if="!matchingSlots.length" class="execution-target__warning">
|
||||
当前角色没有匹配的设备用途,请先打开开始节点添加。
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
import { getDeviceKindForAction } from '@/utils/robotDevice'
|
||||
import { actionRequiresDevice, getDeviceKindName, getResourceRoles } from '@/utils/flowResources'
|
||||
|
||||
const props = defineProps({
|
||||
data: { type: Object, required: true },
|
||||
modelValue: { type: Object, default: () => ({}) },
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const roles = ref([])
|
||||
const localTarget = reactive({ roleKey: '', dispatchPolicy: 'ONE', deviceSlotKey: '' })
|
||||
const deviceSlotKey = ref('')
|
||||
|
||||
const hasDeviceParameter = computed(() => (
|
||||
actionRequiresDevice(props.data.properties?.action)
|
||||
))
|
||||
const requiredDeviceKind = computed(() => getDeviceKindForAction(props.data.properties?.action))
|
||||
const selectedRole = computed(() => roles.value.find((role) => role.roleKey === localTarget.roleKey))
|
||||
const matchingSlots = computed(() => (selectedRole.value?.deviceSlots || []).filter((slot) => (
|
||||
requiredDeviceKind.value == null
|
||||
|| slot.deviceKind == null
|
||||
|| Number(slot.deviceKind) === Number(requiredDeviceKind.value)
|
||||
)))
|
||||
|
||||
const emitTarget = () => {
|
||||
emit('update:modelValue', {
|
||||
roleKey: localTarget.roleKey,
|
||||
dispatchPolicy: 'ONE',
|
||||
deviceSlotKey: hasDeviceParameter.value ? deviceSlotKey.value : '',
|
||||
})
|
||||
}
|
||||
|
||||
const selectDefaultSlot = () => {
|
||||
const currentValid = matchingSlots.value.some((slot) => slot.slotKey === deviceSlotKey.value)
|
||||
if (!currentValid) deviceSlotKey.value = matchingSlots.value.length === 1 ? matchingSlots.value[0].slotKey : ''
|
||||
}
|
||||
|
||||
const handleRoleChange = () => {
|
||||
deviceSlotKey.value = ''
|
||||
selectDefaultSlot()
|
||||
emitTarget()
|
||||
}
|
||||
|
||||
const init = () => {
|
||||
roles.value = getResourceRoles()
|
||||
const value = props.modelValue || {}
|
||||
localTarget.roleKey = roles.value.some((role) => role.roleKey === value.roleKey)
|
||||
? value.roleKey
|
||||
: roles.value[0]?.roleKey || ''
|
||||
localTarget.dispatchPolicy = value.dispatchPolicy || 'ONE'
|
||||
deviceSlotKey.value = value.deviceSlotKey || value.deviceBindings?.[0]?.slotKey || ''
|
||||
selectDefaultSlot()
|
||||
emitTarget()
|
||||
}
|
||||
|
||||
watch(() => props.data.id, init, { immediate: true })
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.execution-target {
|
||||
margin: 12px 0 2px;
|
||||
padding: 14px 0 2px;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
.execution-target__heading {
|
||||
margin-bottom: 12px;
|
||||
color: var(--el-text-color-primary);
|
||||
font-size: 14px;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.execution-target__warning {
|
||||
margin-top: 6px;
|
||||
color: var(--el-color-warning-dark-2);
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
</style>
|
||||
@ -1,207 +1,227 @@
|
||||
<!--
|
||||
* 文件说明:开始节点参数面板,配置流程入口参数。
|
||||
* 作用范围:仅服务于流程设计器模块。
|
||||
-->
|
||||
<!-- 开始节点维护流程级机器人角色和业务输入参数。 -->
|
||||
<template>
|
||||
<el-collapse v-model="activeNames">
|
||||
<el-collapse-item name="1" icon-position="left">
|
||||
<template #title>
|
||||
输入参数
|
||||
<el-button :circle="true" size="small" @click="(e) => addFormItem(e, 'inputParams')" class="addFormItem">
|
||||
<el-icon :size="14">
|
||||
<Plus />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</template>
|
||||
<el-form :inline="true" :model="formData" :rules="rules" ref="dynamicFormRef" 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>
|
||||
<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 { ref, reactive, watch, nextTick } from 'vue'
|
||||
import { Plus, Minus } from '@element-plus/icons-vue'
|
||||
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 { getInput } from '@/utils/flow'
|
||||
import { useQuote } from './useQuote.js'
|
||||
|
||||
const props = defineProps({
|
||||
data: Object
|
||||
})
|
||||
import {
|
||||
createDefaultRobotRole,
|
||||
createDeviceSlot,
|
||||
createResourceKey,
|
||||
normalizeFlowResourceSchema,
|
||||
} from '@/utils/flowResources'
|
||||
|
||||
const props = defineProps({ data: Object })
|
||||
const emit = defineEmits(['save-success', 'save-error'])
|
||||
|
||||
const formData = reactive({
|
||||
inputParams: [],
|
||||
})
|
||||
const deviceKinds = [
|
||||
{ label: '移动底盘', value: 1 },
|
||||
{ label: '机械臂', value: 2 },
|
||||
{ label: '仿生头', value: 4 },
|
||||
{ label: '摄像头', value: 5 },
|
||||
{ label: '麦克风', value: 9 },
|
||||
{ label: '机器人本体', value: 12 },
|
||||
{ label: '扬声器', value: 13 },
|
||||
]
|
||||
|
||||
const rules = reactive({})
|
||||
const formData = reactive({ resourceRoles: [], inputParams: [] })
|
||||
const dynamicFormRef = ref()
|
||||
const activeNames = ref(['roles', 'inputs'])
|
||||
|
||||
const activeNames = ref(['1', '2'])
|
||||
|
||||
// 添加表单项
|
||||
/**
|
||||
* 新增 addFormItem 对应的数据或交互;作用范围仅限当前组件或模块。
|
||||
* @param {*} e 触发操作的浏览器事件
|
||||
* @param {*} type 目标类型
|
||||
*/
|
||||
const addFormItem = (e, type) => {
|
||||
e.stopPropagation()
|
||||
const obj = {
|
||||
name: '',
|
||||
type: type === 'nodeParams' ? 'input' : 'string',
|
||||
required: false,
|
||||
children: [],
|
||||
input: ''
|
||||
}
|
||||
if (!formData[type]) formData[type] = []
|
||||
formData[type].push(obj)
|
||||
const addRole = () => {
|
||||
formData.resourceRoles.push({
|
||||
...createDefaultRobotRole(),
|
||||
roleKey: createResourceKey('robot_role'),
|
||||
displayName: `机器人角色${formData.resourceRoles.length + 1}`,
|
||||
})
|
||||
}
|
||||
|
||||
// 删除输入参数(非递归)
|
||||
/**
|
||||
* 处理 handleDelete 对应的数据或交互;作用范围仅限当前组件或模块。
|
||||
* @param {*} index 目标项索引
|
||||
*/
|
||||
const handleDelete = (index) => {
|
||||
formData.nodeParams.splice(index, 1)
|
||||
const addDeviceSlot = (role) => {
|
||||
role.deviceSlots.push(createDeviceSlot(null, role.deviceSlots))
|
||||
}
|
||||
|
||||
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: '' })
|
||||
}
|
||||
|
||||
// 删除顶层输出参数(递归删除通过 FormItemRecursive 的 emit 处理)
|
||||
/**
|
||||
* 删除 deleteTopLevelItem 对应的数据或交互;作用范围仅限当前组件或模块。
|
||||
* @param {*} fullPath 字段完整路径
|
||||
* @param {*} propPath 字段校验路径
|
||||
*/
|
||||
const deleteTopLevelItem = (fullPath, propPath) => {
|
||||
let currentLevel = formData[propPath]
|
||||
for (let i = 0; i < fullPath.length - 1; i++) {
|
||||
currentLevel = currentLevel[fullPath[i]].children
|
||||
}
|
||||
const lastIndex = fullPath[fullPath.length - 1]
|
||||
currentLevel.splice(lastIndex, 1)
|
||||
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)
|
||||
}
|
||||
|
||||
// 初始化数据
|
||||
/**
|
||||
* 初始化 initData 对应的数据或交互;作用范围仅限当前组件或模块。
|
||||
*/
|
||||
const initData = () => {
|
||||
const inputParams = JSON.parse(JSON.stringify(props.data.properties.inputParams || []))
|
||||
formData.inputParams = inputParams;
|
||||
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,
|
||||
() => {
|
||||
initData()
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
)
|
||||
watch(() => props.data.id, initData, { immediate: true })
|
||||
|
||||
// 保存验证
|
||||
/**
|
||||
* 校验 validateAndSave 对应的数据或交互;作用范围仅限当前组件或模块。
|
||||
*/
|
||||
const validateAndSave = async () => {
|
||||
let inputValid = true
|
||||
if (dynamicFormRef.value) {
|
||||
await dynamicFormRef.value.validate((valid) => { if (!valid) inputValid = false })
|
||||
}
|
||||
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
|
||||
)))
|
||||
|
||||
// 保存到 lf
|
||||
lf.setProperties(props.data.id, {
|
||||
...props.data.properties,
|
||||
...formData
|
||||
})
|
||||
if (inputValid) {
|
||||
emit('save-success')
|
||||
} else {
|
||||
emit('save-error')
|
||||
}
|
||||
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 lang="scss" scoped>
|
||||
.form__container {
|
||||
margin: 12px 0;
|
||||
<style scoped lang="scss">
|
||||
.role-intro {
|
||||
margin-bottom: 14px;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
:deep(.el-row) {
|
||||
align-items: end;
|
||||
.role-row {
|
||||
margin-bottom: 14px;
|
||||
padding: 14px;
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.el-form-item {
|
||||
margin-right: 12px;
|
||||
}
|
||||
.role-row__header,
|
||||
.device-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.param-name {
|
||||
width: 160px;
|
||||
}
|
||||
.role-row__section {
|
||||
margin-top: 14px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
.param-type {
|
||||
width: 80px;
|
||||
}
|
||||
.role-row__label {
|
||||
margin-bottom: 8px;
|
||||
color: var(--el-text-color-regular);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.param-value {
|
||||
width: 160px;
|
||||
}
|
||||
.device-row {
|
||||
grid-template-columns: minmax(0, 1fr) 132px auto;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.sub-properties {
|
||||
margin-left: 10px;
|
||||
|
||||
.zw {
|
||||
margin-left: 15px;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: -4px;
|
||||
width: 10px;
|
||||
border-left: 1px solid gray;
|
||||
border-bottom: 1px solid gray;
|
||||
border-bottom-left-radius: 4px;
|
||||
background-color: transparent;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.gSon-properties {
|
||||
margin-left: 10px;
|
||||
|
||||
.zw {
|
||||
margin-left: 15px;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: -4px;
|
||||
width: 10px;
|
||||
border-left: 1px solid gray;
|
||||
border-bottom: 1px solid gray;
|
||||
border-bottom-left-radius: 4px;
|
||||
background-color: transparent;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.deleteBtn {
|
||||
margin-bottom: 22px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.empty-tip {
|
||||
margin-bottom: 6px;
|
||||
color: var(--el-text-color-placeholder);
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -107,9 +107,7 @@ export const collapseList = [
|
||||
desc: "在获取相机照片时,需要先启动相机",
|
||||
action: 'CAMERA_START',
|
||||
nodeType: 'EDGE',
|
||||
nodeParams: [
|
||||
{ name: "deviceId", type: "input", input: "", componentType: 'select', selectOptions: [], disabled: true }
|
||||
],
|
||||
nodeParams: [],
|
||||
outputParams: [],
|
||||
outputType: 'json'
|
||||
},
|
||||
@ -120,9 +118,7 @@ export const collapseList = [
|
||||
desc: "获取相机拍摄的照片",
|
||||
action: 'CAMERA_GETRGBIMAGE',
|
||||
nodeType: 'EDGE',
|
||||
nodeParams: [
|
||||
{ name: "deviceId", type: "input", input: "", componentType: 'select', selectOptions: [], disabled: true }
|
||||
],
|
||||
nodeParams: [],
|
||||
outputType: 'img',
|
||||
outputParams: [{ name: 'imageUrl', type: 'array<string>', desc: '图片地址数组', children: [], disabled: true }]
|
||||
},
|
||||
@ -133,9 +129,7 @@ export const collapseList = [
|
||||
desc: "获取相机照片后,关闭该相机",
|
||||
action: 'CAMERA_STOP',
|
||||
nodeType: 'EDGE',
|
||||
nodeParams: [
|
||||
{ name: "deviceId", type: "input", input: "", componentType: 'select', selectOptions: [], disabled: true }
|
||||
],
|
||||
nodeParams: [],
|
||||
outputParams: [],
|
||||
outputType: 'json'
|
||||
},
|
||||
@ -151,9 +145,7 @@ export const collapseList = [
|
||||
desc: "开启摄像头,并开始录像",
|
||||
action: 'CAMERA_RECORDING_START',
|
||||
nodeType: 'EDGE',
|
||||
nodeParams: [
|
||||
{ name: "deviceId", type: "input", input: "", componentType: 'select', selectOptions: [], disabled: true }
|
||||
],
|
||||
nodeParams: [],
|
||||
outputParams: [],
|
||||
outputType: 'json'
|
||||
},
|
||||
@ -164,9 +156,7 @@ export const collapseList = [
|
||||
desc: "停止录像,并关闭摄像头",
|
||||
action: 'CAMERA_RECORDING_STOP',
|
||||
nodeType: 'EDGE',
|
||||
nodeParams: [
|
||||
{ name: "deviceId", type: "input", input: "", componentType: 'select', selectOptions: [], disabled: true }
|
||||
],
|
||||
nodeParams: [],
|
||||
outputType: 'video',
|
||||
outputParams: [{ name: 'videoUrl', type: 'array<string>', children: [], desc: '视频播放地址数组', disabled: true}]
|
||||
}
|
||||
@ -314,7 +304,6 @@ function handler(params) {
|
||||
action: 'AGV_MOVE_TO_POINT',
|
||||
nodeType: 'EDGE',
|
||||
nodeParams: [
|
||||
{ name: "deviceId", type: "input", input: "", disabled: true },
|
||||
{ name: "x", type: "input", input: "", disabled: true },
|
||||
{ name: "y", type: "input", input: "", disabled: true },
|
||||
{ name: "theta", type: "input", input: "", disabled: true },
|
||||
@ -329,7 +318,6 @@ function handler(params) {
|
||||
action: 'AGV_MOVE_TO_STATION',
|
||||
nodeType: 'EDGE',
|
||||
nodeParams: [
|
||||
{ name: "deviceId", type: "input", input: "", disabled: true },
|
||||
{ name: "stationId", type: "input", input: "", componentType: 'select', selectOptions: [], forceSelect: true, disabled: true }
|
||||
],
|
||||
outputType: 'json'
|
||||
@ -342,7 +330,6 @@ function handler(params) {
|
||||
action: 'DEVICE_EXECUTE_JSON_COMMAND',
|
||||
nodeType: 'EDGE',
|
||||
nodeParams: [
|
||||
{ name: "deviceId", type: "input", input: "", componentType: 'select', selectOptions: [], required: true, disabled: true },
|
||||
{ name: "requestJson", type: "input", input: `{
|
||||
"a":123
|
||||
}`, required: false, disabled: true },
|
||||
@ -358,7 +345,6 @@ function handler(params) {
|
||||
action: 'ARM_MOVE_TO_POINT',
|
||||
nodeType: 'EDGE',
|
||||
nodeParams: [
|
||||
{ name: "deviceId", type: "input", input: "", disabled: true },
|
||||
{ name: "x", type: "input", input: "", disabled: true },
|
||||
{ name: "y", type: "input", input: "", disabled: true },
|
||||
{ name: "z", type: "input", input: "", disabled: true },
|
||||
@ -378,7 +364,6 @@ function handler(params) {
|
||||
action: 'ARM_MOVE_TO_J',
|
||||
nodeType: 'EDGE',
|
||||
nodeParams: [
|
||||
{ name: "deviceId", type: "input", input: "", disabled: true },
|
||||
{ name: "target", type: "input", input: "[0, 0, 0, 0, 0, 0]", disabled: true },
|
||||
{ name: "velocity", type: "input", componentType: 'number', max: 5, step: 0.01, input: 0.1, disabled: true },
|
||||
{ name: "acceleration", type: "input", componentType: 'number', max: 5, step: 0.01, input: 0.2, disabled: true },
|
||||
@ -457,9 +442,7 @@ function handler(params) {
|
||||
desc: "用于启动麦克风的节点",
|
||||
action: 'MICROPHONE_START',
|
||||
nodeType: 'EDGE',
|
||||
nodeParams: [
|
||||
{ name: "deviceId", type: "input", input: "", componentType: 'select', selectOptions: [], disabled: true }
|
||||
],
|
||||
nodeParams: [],
|
||||
outputType: 'json'
|
||||
},
|
||||
{
|
||||
@ -469,9 +452,7 @@ function handler(params) {
|
||||
desc: "用于停止麦克风的节点",
|
||||
action: 'MICROPHONE_STOP',
|
||||
nodeType: 'EDGE',
|
||||
nodeParams: [
|
||||
{ name: "deviceId", type: "input", input: "", componentType: 'select', selectOptions: [], disabled: true }
|
||||
],
|
||||
nodeParams: [],
|
||||
outputType: 'json'
|
||||
},
|
||||
{
|
||||
@ -482,7 +463,6 @@ function handler(params) {
|
||||
action: 'SPEAKER_PLAYAUDIO',
|
||||
nodeType: 'EDGE',
|
||||
nodeParams: [
|
||||
{ name: "deviceId", type: "input", input: "", componentType: 'select', selectOptions: [], disabled: true },
|
||||
{ name: "audioPath", type: "input", input: "", disabled: true }
|
||||
],
|
||||
outputType: 'json'
|
||||
@ -636,13 +616,12 @@ function handler(params) {
|
||||
icon: audioSvg,
|
||||
name: "播放语料",
|
||||
type: "serviceNode",
|
||||
desc: "用于播放语料的节点,需要输入语音路径(audioPath)和播放语音的设备id(deviceId)",
|
||||
desc: "用于播放语料的节点,需要输入语音路径,执行设备由流程角色配置",
|
||||
action: 'VI_PLAY_CORPUS',
|
||||
nodeType: "EDGE",
|
||||
outputType: 'json',
|
||||
nodeParams: [
|
||||
{ name: 'audioPath', type: "input", input: "", disabled: true },
|
||||
{ name: 'deviceId', type: "input", input: "", disabled: true },
|
||||
{ name: 'bioId', type: "input", input: "", disabled: true, required: false }
|
||||
],
|
||||
}
|
||||
@ -659,7 +638,6 @@ function handler(params) {
|
||||
action: 'TOUCH',
|
||||
nodeType: "EDGE",
|
||||
nodeParams: [
|
||||
{ name: "deviceId", type: "input", input: "", disabled: true },
|
||||
{ name: "touchParams", type: "input", input: "", disabled: true }
|
||||
],
|
||||
outputType: 'json',
|
||||
@ -671,9 +649,7 @@ function handler(params) {
|
||||
desc: "控制机器人说话",
|
||||
action: 'BIO_HEAD_SPEAK_START',
|
||||
nodeType: "EDGE",
|
||||
nodeParams: [
|
||||
{ name: "deviceId", type: "input", input: "", disabled: true }
|
||||
],
|
||||
nodeParams: [],
|
||||
outputType: 'json',
|
||||
},
|
||||
{
|
||||
@ -683,9 +659,7 @@ function handler(params) {
|
||||
desc: "停止机器人说话",
|
||||
action: 'BIO_HEAD_SPEAK_STOP',
|
||||
nodeType: "EDGE",
|
||||
nodeParams: [
|
||||
{ name: "deviceId", type: "input", input: "", disabled: true }
|
||||
],
|
||||
nodeParams: [],
|
||||
outputType: 'json',
|
||||
},
|
||||
{
|
||||
@ -696,7 +670,6 @@ function handler(params) {
|
||||
action: 'BIO_HEAD_SPECIAL_EXPRESSION',
|
||||
nodeType: "EDGE",
|
||||
nodeParams: [
|
||||
{ name: "deviceId", type: "input", input: "", disabled: true },
|
||||
{ name: "expressKey", type: "input",componentType: 'select', selectOptions: expressOptions(), disabled: true },
|
||||
],
|
||||
outputType: 'json',
|
||||
|
||||
@ -268,6 +268,15 @@ import { ElMessage } from "element-plus";
|
||||
import { useFlowStore } from "@/store/modules/flow";
|
||||
import { emitter } from "@/utils/eventBus";
|
||||
import { convertToTree } from "@/utils/flow";
|
||||
import {
|
||||
actionRequiresDevice,
|
||||
createDefaultRobotRole,
|
||||
ensureNodeExecutionResources,
|
||||
getResourceRoles,
|
||||
mergeCopiedResourceRoles,
|
||||
normalizeFlowResourceSchema,
|
||||
validateFlowResources,
|
||||
} from "@/utils/flowResources";
|
||||
import {
|
||||
flowDeploy,
|
||||
flowView,
|
||||
@ -349,6 +358,13 @@ const drop = (e) => {
|
||||
...other,
|
||||
},
|
||||
});
|
||||
const normalizedResources = ensureNodeExecutionResources(lf.getGraphData(), newNode.id);
|
||||
if (normalizedResources.start) {
|
||||
lf.setProperties(normalizedResources.start.id, normalizedResources.start.properties);
|
||||
}
|
||||
if (normalizedResources.node) {
|
||||
lf.setProperties(normalizedResources.node.id, normalizedResources.node.properties);
|
||||
}
|
||||
if (loopNode && !["start", "end", "loop", "selectArea"].includes(newNode.type)) {
|
||||
lf.addToGroup(loopNode.id, newNode.id, point.canvasOverlayPosition);
|
||||
}
|
||||
@ -357,7 +373,7 @@ const drop = (e) => {
|
||||
!["selectArea", "stopLoop", "subStart", "subEnd"].includes(newNode.type)
|
||||
) {
|
||||
showParamsDrawer.value = true;
|
||||
paramsDrawerData.value = newNode;
|
||||
paramsDrawerData.value = lf.getGraphData().nodes.find((item) => item.id === newNode.id) || newNode;
|
||||
}
|
||||
};
|
||||
|
||||
@ -569,6 +585,17 @@ const testRun = async () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const resourceValidation = validateFlowResources({ nodes, edges });
|
||||
if (resourceValidation.errors.length > 0) {
|
||||
const error = resourceValidation.errors[0];
|
||||
ElMessage.warning(error.message);
|
||||
if (error.nodeId) {
|
||||
lf.selectElementById(error.nodeId);
|
||||
lf.focusOn({ id: error.nodeId });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
isOpen.value = true;
|
||||
};
|
||||
|
||||
@ -649,9 +676,9 @@ const initFlowData = {
|
||||
properties: {
|
||||
name: "Start",
|
||||
action: "start",
|
||||
inputParams: [
|
||||
{ name: "robotId", type: "string", input: "", disabled: true }
|
||||
],
|
||||
schemaVersion: 2,
|
||||
resourceRoles: [createDefaultRobotRole()],
|
||||
inputParams: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -824,7 +851,7 @@ const loadFlowData = async (id, lf) => {
|
||||
try {
|
||||
const res = await getDetect(id);
|
||||
if (res.data && res.data.flowData) {
|
||||
const flowData = JSON.parse(res.data.flowData);
|
||||
const flowData = normalizeFlowResourceSchema(JSON.parse(res.data.flowData));
|
||||
lf.render(flowData);
|
||||
|
||||
nextTick(() => {
|
||||
@ -1075,19 +1102,9 @@ const handleSingleNodeRobotChange = async (robotId) => {
|
||||
}
|
||||
deviceParam.componentType = "select";
|
||||
deviceParam.selectOptions = toDeviceOptions(devices);
|
||||
if (deviceKind) {
|
||||
const currentDevice = devices.find((device) => device.deviceId === originalDeviceId);
|
||||
deviceParam.input = (currentDevice || devices[0]).deviceId;
|
||||
deviceParam.deviceLocked = devices.length === 1;
|
||||
return;
|
||||
}
|
||||
|
||||
const originalStillAvailable = devices.some((device) => device.deviceId === originalDeviceId);
|
||||
deviceParam.input = originalDeviceId || "";
|
||||
deviceParam.deviceLocked = devices.length === 1 && originalStillAvailable;
|
||||
if (originalDeviceId && !originalStillAvailable) {
|
||||
ElMessage.warning("已保留通用指令原设备;该设备未出现在当前机器人的在线列表中");
|
||||
}
|
||||
const currentDevice = devices.find((device) => device.deviceId === originalDeviceId);
|
||||
deviceParam.input = (currentDevice || devices[0]).deviceId;
|
||||
deviceParam.deviceLocked = devices.length === 1;
|
||||
} catch (error) {
|
||||
deviceParam.input = originalDeviceId;
|
||||
deviceParam.selectOptions = originalOptions;
|
||||
@ -1153,7 +1170,8 @@ const loadSingleNodeRobots = async () => {
|
||||
const singleNodeExecution = async (e) => {
|
||||
rememberSingleNodeRobot();
|
||||
const { name, nodeType, nodeParams, action, nodeId } = e.detail;
|
||||
const executionParams = JSON.parse(JSON.stringify(nodeParams || []));
|
||||
const executionParams = JSON.parse(JSON.stringify(nodeParams || []))
|
||||
.filter((param) => !['robotId', 'deviceId'].includes(param.name));
|
||||
const normalizedAction = String(action || "").toUpperCase();
|
||||
const effectiveNodeType = nodeType || (getDeviceKindForAction(normalizedAction) ? "EDGE" : "");
|
||||
activeSingleNodeId.value = nodeId || `${normalizedAction}:${name || ""}`;
|
||||
@ -1164,7 +1182,7 @@ const singleNodeExecution = async (e) => {
|
||||
stationLoading.value = false;
|
||||
if (effectiveNodeType === "EDGE") {
|
||||
await loadSingleNodeRobots();
|
||||
formData.nodeParams = [
|
||||
const routingParams = [
|
||||
{
|
||||
disabled: true,
|
||||
input: "",
|
||||
@ -1173,8 +1191,18 @@ const singleNodeExecution = async (e) => {
|
||||
componentType: "select",
|
||||
selectOptions: singleNodeRobotOptions.value,
|
||||
},
|
||||
...executionParams,
|
||||
];
|
||||
if (actionRequiresDevice(normalizedAction)) {
|
||||
routingParams.push({
|
||||
disabled: true,
|
||||
input: "",
|
||||
name: "deviceId",
|
||||
type: "input",
|
||||
componentType: "select",
|
||||
selectOptions: [],
|
||||
});
|
||||
}
|
||||
formData.nodeParams = [...routingParams, ...executionParams];
|
||||
const cachedRobotId = singleNodeRobotCache.get(activeSingleNodeId.value) || lastSingleNodeRobotId;
|
||||
if (cachedRobotId) {
|
||||
formData.nodeParams[0].input = cachedRobotId;
|
||||
@ -1246,7 +1274,14 @@ const clearRun = () => {
|
||||
|
||||
const NODE_PASTE_OFFSET = 40;
|
||||
const NON_COPYABLE_NODE_TYPES = new Set(["start", "end", "selectArea"]);
|
||||
let copiedGraph = { nodes: [], edges: [] };
|
||||
const FLOW_CLIPBOARD_TYPE = "cmvr-flow/nodes";
|
||||
const FLOW_CLIPBOARD_VERSION = 1;
|
||||
const FLOW_CLIPBOARD_MIME = "application/x-cmvr-flow-nodes";
|
||||
const FLOW_CLIPBOARD_STORAGE_KEY = "cmvr-flow:clipboard";
|
||||
const FLOW_EDITOR_INSTANCE_ID = uuid();
|
||||
const NODE_REFERENCE_ARRAY_KEYS = new Set(["quote", "nameQuote"]);
|
||||
let copiedPayload = null;
|
||||
let lastPastedClipboardId = "";
|
||||
let pasteCount = 0;
|
||||
|
||||
const EDITABLE_SELECTOR = [
|
||||
@ -1289,6 +1324,7 @@ const isTextEditing = (event) => {
|
||||
const copySelectedNodes = (event) => {
|
||||
// 复制只保留选中节点及它们之间的内部连线,避免粘贴时重建指向未选节点的关系。
|
||||
// 节点数据在此深拷贝,后续生成新 ID 时不会反向污染原画布对象。
|
||||
if (flowStore.disableForm || isTextEditing(event)) return;
|
||||
const selectedNodes = lf.getSelectElements().nodes;
|
||||
if (!selectedNodes.length) return;
|
||||
|
||||
@ -1321,9 +1357,79 @@ const copySelectedNodes = (event) => {
|
||||
copyableIds.has(edge.sourceNodeId) && copyableIds.has(edge.targetNodeId)
|
||||
));
|
||||
|
||||
const payload = {
|
||||
type: FLOW_CLIPBOARD_TYPE,
|
||||
version: FLOW_CLIPBOARD_VERSION,
|
||||
clipboardId: uuid(),
|
||||
sourceEditorId: FLOW_EDITOR_INSTANCE_ID,
|
||||
sourceFlowId: flowInfoData.value.itemId || null,
|
||||
copiedAt: Date.now(),
|
||||
resourceRoles: getResourceRoles(graph),
|
||||
graph: JSON.parse(JSON.stringify({ nodes, edges })),
|
||||
};
|
||||
const serialized = JSON.stringify(payload);
|
||||
|
||||
event.preventDefault();
|
||||
copiedGraph = JSON.parse(JSON.stringify({ nodes, edges }));
|
||||
if (event.clipboardData) {
|
||||
try {
|
||||
event.clipboardData.setData(FLOW_CLIPBOARD_MIME, serialized);
|
||||
} catch (error) {
|
||||
console.debug("浏览器不支持自定义流程剪贴板类型,使用文本格式", error);
|
||||
}
|
||||
event.clipboardData.setData("text/plain", serialized);
|
||||
}
|
||||
copiedPayload = payload;
|
||||
try {
|
||||
localStorage.setItem(FLOW_CLIPBOARD_STORAGE_KEY, serialized);
|
||||
} catch (error) {
|
||||
console.debug("流程剪贴板本地缓存写入失败", error);
|
||||
}
|
||||
pasteCount = 0;
|
||||
lastPastedClipboardId = "";
|
||||
};
|
||||
|
||||
/**
|
||||
* 解析并校验流程剪贴板内容,避免把普通 JSON 文本当作流程节点粘贴。
|
||||
* @param {*} raw 剪贴板原始文本
|
||||
*/
|
||||
const parseFlowClipboardPayload = (raw) => {
|
||||
if (!raw || typeof raw !== "string") return null;
|
||||
try {
|
||||
const payload = JSON.parse(raw);
|
||||
if (
|
||||
payload?.type !== FLOW_CLIPBOARD_TYPE
|
||||
|| payload?.version !== FLOW_CLIPBOARD_VERSION
|
||||
|| !Array.isArray(payload?.graph?.nodes)
|
||||
|| !Array.isArray(payload?.graph?.edges)
|
||||
|| payload.graph.nodes.length === 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return payload;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 优先读取系统剪贴板;浏览器未暴露剪贴板数据时使用同源窗口共享缓存。
|
||||
* @param {*} event 粘贴事件
|
||||
*/
|
||||
const readFlowClipboardPayload = (event) => {
|
||||
const clipboard = event.clipboardData;
|
||||
if (clipboard) {
|
||||
const customText = clipboard.getData(FLOW_CLIPBOARD_MIME);
|
||||
const plainText = clipboard.getData("text/plain");
|
||||
const raw = customText || plainText;
|
||||
// 系统剪贴板存在其他内容时,不应使用一份过期的本地流程缓存。
|
||||
if (raw) return parseFlowClipboardPayload(raw);
|
||||
}
|
||||
if (copiedPayload) return copiedPayload;
|
||||
try {
|
||||
return parseFlowClipboardPayload(localStorage.getItem(FLOW_CLIPBOARD_STORAGE_KEY));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@ -1343,20 +1449,57 @@ const remapNodeAnchorId = (anchorId, oldNodeId, newNodeId) => {
|
||||
* @param {*} idMap 节点 ID 映射
|
||||
* @param {*} parentKey 递归父键名
|
||||
*/
|
||||
const remapCopiedNodeReferences = (value, idMap, parentKey = "") => {
|
||||
const remapCopiedNodeReferences = (value, idMap, parentKey = "", options = {}) => {
|
||||
if (Array.isArray(value)) {
|
||||
const result = value.map((item) => remapCopiedNodeReferences(item, idMap));
|
||||
if (parentKey === "quote" && result.length && idMap.has(result[0])) {
|
||||
result[0] = idMap.get(result[0]);
|
||||
const result = value.map((item) => remapCopiedNodeReferences(item, idMap, "", options));
|
||||
if (NODE_REFERENCE_ARRAY_KEYS.has(parentKey) && result.length) {
|
||||
if (idMap.has(result[0])) {
|
||||
result[0] = idMap.get(result[0]);
|
||||
} else if (options.dropExternalReferences) {
|
||||
options.removedReferenceCount += 1;
|
||||
return [];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
if (!value || typeof value !== "object") return value;
|
||||
return Object.fromEntries(Object.entries(value).map(([key, item]) => (
|
||||
[key, remapCopiedNodeReferences(item, idMap, key)]
|
||||
[key, remapCopiedNodeReferences(item, idMap, key, options)]
|
||||
)));
|
||||
};
|
||||
|
||||
/**
|
||||
* 跨窗口粘贴时将节点组移动到当前画布视口中心;当前窗口内复制仍使用固定偏移。
|
||||
* @param {*} payload 流程剪贴板载荷
|
||||
*/
|
||||
const resolvePasteTranslation = (payload) => {
|
||||
if (payload.sourceEditorId === FLOW_EDITOR_INSTANCE_ID) {
|
||||
const offset = NODE_PASTE_OFFSET * pasteCount;
|
||||
return { x: offset, y: offset };
|
||||
}
|
||||
|
||||
const nodes = payload.graph.nodes;
|
||||
const xs = nodes.map((node) => Number(node.x || 0));
|
||||
const ys = nodes.map((node) => Number(node.y || 0));
|
||||
const sourceCenter = {
|
||||
x: (Math.min(...xs) + Math.max(...xs)) / 2,
|
||||
y: (Math.min(...ys) + Math.max(...ys)) / 2,
|
||||
};
|
||||
const canvas = document.getElementById("main-flow");
|
||||
if (!canvas) {
|
||||
const offset = NODE_PASTE_OFFSET * pasteCount;
|
||||
return { x: offset, y: offset };
|
||||
}
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const target = lf.getPointByClient(rect.left + rect.width / 2, rect.top + rect.height / 2)
|
||||
.canvasOverlayPosition;
|
||||
const repeatedOffset = NODE_PASTE_OFFSET * (pasteCount - 1);
|
||||
return {
|
||||
x: target.x - sourceCenter.x + repeatedOffset,
|
||||
y: target.y - sourceCenter.y + repeatedOffset,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 粘贴 pasteCopiedNodes 对应的数据或交互;作用范围仅限当前组件或模块。
|
||||
* @param {*} event 触发操作的浏览器事件
|
||||
@ -1364,26 +1507,42 @@ const remapCopiedNodeReferences = (value, idMap, parentKey = "") => {
|
||||
const pasteCopiedNodes = (event) => {
|
||||
// 先为全部节点建立旧 ID 到新 ID 的映射,再重建父子引用、锚点和内部连线。
|
||||
// 该顺序保证循环、分组和分支之间的交叉引用都能解析到新的目标 ID。
|
||||
if (!copiedGraph.nodes.length || flowStore.disableForm) return;
|
||||
if (flowStore.disableForm || isTextEditing(event)) return;
|
||||
const payload = readFlowClipboardPayload(event);
|
||||
if (!payload) return;
|
||||
const copiedGraph = payload.graph;
|
||||
|
||||
event.preventDefault();
|
||||
if (lastPastedClipboardId !== payload.clipboardId) {
|
||||
pasteCount = 0;
|
||||
lastPastedClipboardId = payload.clipboardId;
|
||||
}
|
||||
pasteCount += 1;
|
||||
const offset = NODE_PASTE_OFFSET * pasteCount;
|
||||
const translation = resolvePasteTranslation(payload);
|
||||
const graph = lf.getGraphData();
|
||||
const existingNodeIds = new Set(graph.nodes.map((node) => node.id));
|
||||
const idMap = new Map(copiedGraph.nodes.map((node) => (
|
||||
[node.id, uuid().replace(/-/g, "")]
|
||||
)));
|
||||
const isSameEditor = payload.sourceEditorId === FLOW_EDITOR_INSTANCE_ID;
|
||||
const isSameFlow = Boolean(payload.sourceFlowId)
|
||||
&& payload.sourceFlowId === flowInfoData.value.itemId;
|
||||
const referenceOptions = {
|
||||
dropExternalReferences: !isSameFlow,
|
||||
removedReferenceCount: 0,
|
||||
};
|
||||
|
||||
const pastedNodes = copiedGraph.nodes.map((sourceNode) => {
|
||||
const newId = idMap.get(sourceNode.id);
|
||||
const properties = remapCopiedNodeReferences(
|
||||
JSON.parse(JSON.stringify(sourceNode.properties || {})),
|
||||
idMap,
|
||||
"",
|
||||
referenceOptions,
|
||||
);
|
||||
const parentId = properties.parentId;
|
||||
if (idMap.has(parentId)) properties.parentId = idMap.get(parentId);
|
||||
else if (!existingNodeIds.has(parentId)) delete properties.parentId;
|
||||
else if (!isSameEditor || !existingNodeIds.has(parentId)) delete properties.parentId;
|
||||
|
||||
if (sourceNode.type === "branch" && Array.isArray(properties.anchor)) {
|
||||
properties.anchor = properties.anchor.map((anchor) => ({
|
||||
@ -1398,8 +1557,8 @@ const pasteCopiedNodes = (event) => {
|
||||
return {
|
||||
...JSON.parse(JSON.stringify(sourceNode)),
|
||||
id: newId,
|
||||
x: Number(sourceNode.x || 0) + offset,
|
||||
y: Number(sourceNode.y || 0) + offset,
|
||||
x: Number(sourceNode.x || 0) + translation.x,
|
||||
y: Number(sourceNode.y || 0) + translation.y,
|
||||
properties,
|
||||
children,
|
||||
};
|
||||
@ -1426,8 +1585,13 @@ const pasteCopiedNodes = (event) => {
|
||||
};
|
||||
});
|
||||
|
||||
const resourceMerge = mergeCopiedResourceRoles(graph, payload.resourceRoles || [], pastedNodes);
|
||||
const graphNodesWithResources = graph.nodes.map((node) => (
|
||||
node.id === resourceMerge.start?.id ? resourceMerge.start : node
|
||||
));
|
||||
|
||||
lf.render({
|
||||
nodes: [...graph.nodes, ...pastedNodes],
|
||||
nodes: [...graphNodesWithResources, ...pastedNodes],
|
||||
edges: [...graph.edges, ...pastedEdges],
|
||||
}, {
|
||||
resetHistory: false,
|
||||
@ -1438,6 +1602,14 @@ const pasteCopiedNodes = (event) => {
|
||||
nextTick(() => {
|
||||
lf.selectElementsByIds(pastedNodes.map((node) => node.id));
|
||||
lf.markChanged();
|
||||
if (referenceOptions.removedReferenceCount > 0) {
|
||||
ElMessage.warning(
|
||||
`已粘贴 ${pastedNodes.length} 个节点,${referenceOptions.removedReferenceCount} 个跨工作流参数引用已清除`,
|
||||
);
|
||||
}
|
||||
if (resourceMerge.addedRoles || resourceMerge.addedSlots) {
|
||||
ElMessage.success(`已同步 ${resourceMerge.addedRoles} 个机器人角色和 ${resourceMerge.addedSlots} 个设备用途`);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@ -1450,15 +1622,6 @@ const handleKeyboardShortcut = (event) => {
|
||||
|
||||
const isModifierKey = event.ctrlKey || event.metaKey;
|
||||
const key = event.key.toLowerCase();
|
||||
if (isModifierKey && key === "c") {
|
||||
if (!flowStore.disableForm) copySelectedNodes(event);
|
||||
return;
|
||||
}
|
||||
if (isModifierKey && key === "v") {
|
||||
pasteCopiedNodes(event);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isModifierKey && key === "z") {
|
||||
if (flowStore.disableForm) return;
|
||||
event.preventDefault();
|
||||
@ -1496,6 +1659,8 @@ onMounted(() => {
|
||||
document.addEventListener("singleNodeExecution", singleNodeExecution);
|
||||
registerBeforeUnload();
|
||||
window.addEventListener("keydown", handleKeyboardShortcut);
|
||||
window.addEventListener("copy", copySelectedNodes);
|
||||
window.addEventListener("paste", pasteCopiedNodes);
|
||||
|
||||
onFlowEvent("openParamsDrawer", (node) => {
|
||||
showParamsDrawer.value = true;
|
||||
@ -1548,6 +1713,8 @@ onUnmounted(() => {
|
||||
|
||||
window.removeEventListener("beforeunload", handleBeforeUnload);
|
||||
window.removeEventListener("keydown", handleKeyboardShortcut);
|
||||
window.removeEventListener("copy", copySelectedNodes);
|
||||
window.removeEventListener("paste", pasteCopiedNodes);
|
||||
document.removeEventListener("singleNodeExecution", singleNodeExecution);
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -32,6 +32,7 @@
|
||||
@sync-anchors="syncBranchAnchors"
|
||||
@add-to-group="addToGroup"
|
||||
/>
|
||||
<div v-if="executionLabel" class="flow-node-executor">{{ executionLabel }}</div>
|
||||
</div>
|
||||
|
||||
<template v-if="flowType === 'branch'">
|
||||
@ -111,6 +112,18 @@ const vueFlow = useVueFlow("main-flow");
|
||||
const componentRef = ref();
|
||||
const flowType = computed(() => props.data.flowType);
|
||||
const properties = computed(() => props.data.properties || {});
|
||||
const executionLabel = computed(() => {
|
||||
const roleKey = properties.value.executionTarget?.roleKey;
|
||||
if (!roleKey) return "";
|
||||
const graph = window.lf?.getGraphData?.();
|
||||
const start = graph?.nodes?.find((node) => node.type === "start");
|
||||
const role = start?.properties?.resourceRoles?.find((item) => item.roleKey === roleKey);
|
||||
if (!role) return "未配置机器人角色";
|
||||
const slotKey = properties.value.executionTarget?.deviceSlotKey
|
||||
|| properties.value.executionTarget?.deviceBindings?.[0]?.slotKey;
|
||||
const slot = role.deviceSlots?.find((item) => item.slotKey === slotKey);
|
||||
return slot ? `${role.displayName} · ${slot.displayName}` : role.displayName;
|
||||
});
|
||||
const nodeComponent = computed(() => componentMap[flowType.value] || ServiceNode);
|
||||
const isGroup = computed(() => ["loop", "selectArea"].includes(flowType.value));
|
||||
const childNodeCount = computed(() => (
|
||||
@ -285,6 +298,16 @@ onMounted(() => bindComponent(componentRef.value));
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.flow-node-executor {
|
||||
overflow: hidden;
|
||||
margin-top: 8px;
|
||||
color: #667085;
|
||||
font-size: 11px;
|
||||
line-height: 16px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.flow-node-card {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
|
||||
@ -143,6 +143,14 @@
|
||||
<el-dialog title="任务执行日志" v-model="logDialogVisible" width="500px" append-to-body>
|
||||
<taskLog :list="logDialogData" :height="300" />
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog title="本次执行安排" v-model="startDialogVisible" width="560px" append-to-body>
|
||||
<RuntimeAssignmentsEditor ref="runtimeEditorRef" v-model="runtimeAssignments" :roles="runtimeRoles" />
|
||||
<template #footer>
|
||||
<el-button @click="startDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="startSubmitting" @click="confirmStartTask">开始执行</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
@ -157,10 +165,20 @@ import { addRunTask, getRunTaskList, startRunTask, pauseRunTask, stopRunTask, re
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { formatDate } from "@/utils/index";
|
||||
import { debounce } from 'lodash'; // 引入 lodash 的防抖函数
|
||||
import { getFlowParamsApi } from '@/api/device/terminal'
|
||||
import { getDetect } from '@/api/test/detect'
|
||||
import RuntimeAssignmentsEditor from '@/views/flow/components/RuntimeAssignmentsEditor.vue'
|
||||
import { getResourceRoles, mergeResourceRoleDefinitions, normalizeFlowResourceSchema } from '@/utils/flowResources'
|
||||
|
||||
const topContainerRef = ref();
|
||||
const containerHeight = useContainerHeight(topContainerRef);
|
||||
const socket = inject('ws');
|
||||
const startDialogVisible = ref(false)
|
||||
const startSubmitting = ref(false)
|
||||
const pendingStartTask = ref(null)
|
||||
const runtimeRoles = ref([])
|
||||
const runtimeAssignments = ref([])
|
||||
const runtimeEditorRef = ref()
|
||||
|
||||
const taskStatusMap = {
|
||||
'0': '已完成',
|
||||
@ -324,11 +342,53 @@ const startTask = async (task) => {
|
||||
getList();
|
||||
}
|
||||
} else {
|
||||
let res = await startRunTask(task.id);
|
||||
if (res.code === 200) {
|
||||
ElMessage.success("任务已开始");
|
||||
getList();
|
||||
const taskDefinition = taskList.value.find(item => item.id === task.taskId)
|
||||
if (!taskDefinition?.taskConfigId) {
|
||||
ElMessage.warning('当前任务没有关联流程编排')
|
||||
return
|
||||
}
|
||||
const response = await getFlowParamsApi(taskDefinition.taskConfigId)
|
||||
const definitions = await Promise.all((response.rows || []).map(async (item) => {
|
||||
const flowResponse = await getDetect(item.itemId)
|
||||
return flowResponse.data?.flowData
|
||||
? getResourceRoles(normalizeFlowResourceSchema(JSON.parse(flowResponse.data.flowData)))
|
||||
: []
|
||||
}))
|
||||
const roles = mergeResourceRoleDefinitions(definitions)
|
||||
runtimeAssignments.value = roles.map((role, index) => ({
|
||||
roleKey: role.roleKey,
|
||||
members: [{
|
||||
memberKey: `${role.roleKey}_1`,
|
||||
robotId: index === 0 ? task.robotId || '' : '',
|
||||
devices: {}
|
||||
}]
|
||||
}))
|
||||
runtimeRoles.value = roles
|
||||
pendingStartTask.value = task
|
||||
startDialogVisible.value = true
|
||||
}
|
||||
}
|
||||
|
||||
const confirmStartTask = async () => {
|
||||
const assignmentError = runtimeEditorRef.value?.validate()
|
||||
if (assignmentError) {
|
||||
ElMessage.warning(assignmentError)
|
||||
return
|
||||
}
|
||||
startSubmitting.value = true
|
||||
try {
|
||||
const robotId = runtimeAssignments.value.flatMap(item => item.members).find(member => member.robotId)?.robotId || ''
|
||||
const res = await startRunTask(pendingStartTask.value.id, {
|
||||
robotId,
|
||||
runtimeAssignments: runtimeAssignments.value
|
||||
})
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('任务已开始')
|
||||
startDialogVisible.value = false
|
||||
getList()
|
||||
}
|
||||
} finally {
|
||||
startSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@ -623,4 +683,4 @@ onUnmounted(() => {
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@ -150,6 +150,15 @@
|
||||
<el-dialog title="任务执行日志" v-model="logDialogVisible" width="500px" append-to-body>
|
||||
<taskLog :list="logDialogData" :height="300" />
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog title="本次执行安排" v-model="startDialogVisible" width="560px" append-to-body>
|
||||
<div class="start-dialog-tip">手机和车辆沿用任务配置,这里只安排执行流程的机器人和设备。</div>
|
||||
<RuntimeAssignmentsEditor ref="runtimeEditorRef" v-model="runtimeAssignments" :roles="runtimeRoles" />
|
||||
<template #footer>
|
||||
<el-button @click="startDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="startSubmitting" @click="confirmStartTask">开始执行</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
@ -174,10 +183,20 @@ import {
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { formatDate } from "@/utils/index";
|
||||
import { debounce } from 'lodash'; // 引入 lodash 的防抖函数
|
||||
import { getFlowParamsApi } from '@/api/device/terminal'
|
||||
import { getDetect } from '@/api/test/detect'
|
||||
import RuntimeAssignmentsEditor from '@/views/flow/components/RuntimeAssignmentsEditor.vue'
|
||||
import { getResourceRoles, mergeResourceRoleDefinitions, normalizeFlowResourceSchema } from '@/utils/flowResources'
|
||||
|
||||
const topContainerRef = ref();
|
||||
const containerHeight = useContainerHeight(topContainerRef);
|
||||
const socket = inject('ws');
|
||||
const startDialogVisible = ref(false)
|
||||
const startSubmitting = ref(false)
|
||||
const pendingStartTask = ref(null)
|
||||
const runtimeRoles = ref([])
|
||||
const runtimeAssignments = ref([])
|
||||
const runtimeEditorRef = ref()
|
||||
|
||||
const taskStatusMap = {
|
||||
'0': '已完成',
|
||||
@ -343,11 +362,45 @@ const startTask = async (task) => {
|
||||
getList();
|
||||
}
|
||||
} else {
|
||||
let res = await startTaskinstance(task.id);
|
||||
if (res.code === 200) {
|
||||
ElMessage.success("任务已开始");
|
||||
getList();
|
||||
const taskDefinition = taskList.value.find(item => item.id === task.taskId)
|
||||
if (!taskDefinition?.taskConfigId) {
|
||||
ElMessage.warning('当前任务没有关联流程编排')
|
||||
return
|
||||
}
|
||||
const response = await getFlowParamsApi(taskDefinition.taskConfigId)
|
||||
const definitions = await Promise.all((response.rows || []).map(async (item) => {
|
||||
const flowResponse = await getDetect(item.itemId)
|
||||
return flowResponse.data?.flowData
|
||||
? getResourceRoles(normalizeFlowResourceSchema(JSON.parse(flowResponse.data.flowData)))
|
||||
: []
|
||||
}))
|
||||
runtimeRoles.value = mergeResourceRoleDefinitions(definitions)
|
||||
runtimeAssignments.value = []
|
||||
pendingStartTask.value = task
|
||||
startDialogVisible.value = true
|
||||
}
|
||||
}
|
||||
|
||||
const confirmStartTask = async () => {
|
||||
const assignmentError = runtimeEditorRef.value?.validate()
|
||||
if (assignmentError) {
|
||||
ElMessage.warning(assignmentError)
|
||||
return
|
||||
}
|
||||
startSubmitting.value = true
|
||||
try {
|
||||
const robotId = runtimeAssignments.value.flatMap(item => item.members).find(member => member.robotId)?.robotId || ''
|
||||
const res = await startTaskinstance(pendingStartTask.value.id, {
|
||||
robotId,
|
||||
runtimeAssignments: runtimeAssignments.value
|
||||
})
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('任务已开始')
|
||||
startDialogVisible.value = false
|
||||
getList()
|
||||
}
|
||||
} finally {
|
||||
startSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@ -644,4 +697,13 @@ onUnmounted(() => {
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
.start-dialog-tip {
|
||||
margin-bottom: 12px;
|
||||
padding: 10px 12px;
|
||||
border-left: 3px solid var(--el-color-primary);
|
||||
background: var(--el-fill-color-light);
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -7,16 +7,9 @@
|
||||
:rules="rules"
|
||||
label-width="auto"
|
||||
>
|
||||
<el-form-item label="执行机器人" prop="formData.robotId" :rules="[{ required: true, message: '请选择在线机器人', trigger: 'change' }]">
|
||||
<el-select v-model="formData.robotId" style="width: 260px" placeholder="请选择在线机器人">
|
||||
<el-option
|
||||
v-for="item in robotIdOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<div class="run-section-title">本次执行安排</div>
|
||||
<RuntimeAssignmentsEditor ref="runtimeEditorRef" v-model="runtimeAssignments" :roles="roles" />
|
||||
<div class="run-section-title run-section-title--params">运行参数</div>
|
||||
<el-table
|
||||
:data="formData.tableData"
|
||||
:border="parentBorder"
|
||||
@ -74,8 +67,10 @@
|
||||
import { ref, watch } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { addValueProperty, formatTableData } from "@/utils/flow";
|
||||
import { getRobotList } from '@/api/inspection/robot'
|
||||
import { getExecuteRunParams, projectExecute, executeProject } from "@/api/is/project/index.js";
|
||||
import { getExecuteRunParams, projectExecute } from "@/api/is/project/index.js";
|
||||
import { getDetect } from '@/api/test/detect'
|
||||
import RuntimeAssignmentsEditor from '@/views/flow/components/RuntimeAssignmentsEditor.vue'
|
||||
import { getResourceRoles, mergeResourceRoleDefinitions, normalizeFlowResourceSchema } from '@/utils/flowResources'
|
||||
|
||||
|
||||
const props = defineProps({
|
||||
@ -90,31 +85,24 @@ const ruleFormRef = ref();
|
||||
const rules = ref({});
|
||||
|
||||
const formData = ref({
|
||||
robotId: '',
|
||||
tableData: []
|
||||
})
|
||||
|
||||
const robotIdOptions = ref([]);
|
||||
|
||||
/** 获取设备终端列表 */
|
||||
const handleGetTerminalGroup = async (row) => {
|
||||
const res = await getRobotList({ pageNum: 1, pageSize: 1000, connectStatus: '1' });
|
||||
if (res.code === 200) {
|
||||
robotIdOptions.value = res.rows?.map((item) => {
|
||||
return {
|
||||
label: `${item.robotName || item.robotId} (${item.robotId})`,
|
||||
value: item.robotId,
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
const roles = ref([])
|
||||
const runtimeAssignments = ref([])
|
||||
const runtimeEditorRef = ref()
|
||||
|
||||
const getProjectParams = async () => {
|
||||
const res = await getExecuteRunParams({ projectId: props.projectId });
|
||||
if (res.code === 200) {
|
||||
const data = parseConfig(res.data)
|
||||
formData.value.tableData = data
|
||||
handleGetTerminalGroup();
|
||||
const definitions = await Promise.all(data.map(async (item) => {
|
||||
const response = await getDetect(item.itemId)
|
||||
return response.data?.flowData
|
||||
? getResourceRoles(normalizeFlowResourceSchema(JSON.parse(response.data.flowData)))
|
||||
: []
|
||||
}))
|
||||
roles.value = mergeResourceRoleDefinitions(definitions)
|
||||
}
|
||||
};
|
||||
|
||||
@ -138,9 +126,18 @@ const confirm = () => {
|
||||
const data = formatTableData(item.config)
|
||||
runParams[item.itemId] = data
|
||||
})
|
||||
const res = await projectExecute({ runParams, projectId: props.projectId, robotId: formData.value.robotId })
|
||||
// 后端临时应付检查添加,后续可删除下一行代码,解开上一行代码注释
|
||||
// const res = await executeProject(props.functionIds)
|
||||
const assignmentError = runtimeEditorRef.value?.validate()
|
||||
if (assignmentError) {
|
||||
ElMessage.warning(assignmentError)
|
||||
return
|
||||
}
|
||||
const robotId = runtimeAssignments.value.flatMap(item => item.members).find(member => member.robotId)?.robotId || ''
|
||||
const res = await projectExecute({
|
||||
runParams,
|
||||
projectId: props.projectId,
|
||||
robotId,
|
||||
runtimeAssignments: runtimeAssignments.value
|
||||
})
|
||||
if (res.code === 200) {
|
||||
ruleFormRef.value.resetFields();
|
||||
ElMessage.success('执行成功')
|
||||
@ -168,3 +165,8 @@ watch(
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.run-section-title { margin: 4px 0 10px; font-size: 15px; font-weight: 650; }
|
||||
.run-section-title--params { margin-top: 22px; }
|
||||
</style>
|
||||
|
||||
@ -7,16 +7,9 @@
|
||||
:rules="rules"
|
||||
label-width="auto"
|
||||
>
|
||||
<el-form-item label="执行机器人" prop="formData.robotId" :rules="[{ required: true, message: '请选择在线机器人', trigger: 'change' }]">
|
||||
<el-select v-model="formData.robotId" style="width: 260px" placeholder="请选择在线机器人">
|
||||
<el-option
|
||||
v-for="item in robotIdOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<div class="run-section-title">本次执行安排</div>
|
||||
<RuntimeAssignmentsEditor ref="runtimeEditorRef" v-model="runtimeAssignments" :roles="roles" />
|
||||
<div class="run-section-title run-section-title--params">运行参数</div>
|
||||
<el-table
|
||||
:data="formData.tableData"
|
||||
:border="parentBorder"
|
||||
@ -74,8 +67,10 @@ import { addValueProperty, formatTableData } from "@/utils/flow";
|
||||
import { emitter } from "@/utils/eventBus";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { getFlowParamsApi } from "@/api/device/terminal.js";
|
||||
import { getRobotList } from '@/api/inspection/robot'
|
||||
import { taskExecute } from "@/api/test/config"
|
||||
import { getDetect } from '@/api/test/detect'
|
||||
import RuntimeAssignmentsEditor from '@/views/flow/components/RuntimeAssignmentsEditor.vue'
|
||||
import { getResourceRoles, mergeResourceRoleDefinitions, normalizeFlowResourceSchema } from '@/utils/flowResources'
|
||||
|
||||
const props = defineProps({
|
||||
drawer: Boolean,
|
||||
@ -88,9 +83,11 @@ const preserveExpanded = ref(false);
|
||||
const ruleFormRef = ref();
|
||||
const rules = ref({});
|
||||
const formData = ref({
|
||||
robotId: '',
|
||||
tableData: []
|
||||
})
|
||||
const roles = ref([])
|
||||
const runtimeAssignments = ref([])
|
||||
const runtimeEditorRef = ref()
|
||||
|
||||
const emits = defineEmits(["close"]);
|
||||
|
||||
@ -104,7 +101,12 @@ const getFlowParams = async () => {
|
||||
if (res.code === 200) {
|
||||
const data = parseConfig(res.rows)
|
||||
formData.value.tableData = data
|
||||
handleGetTerminalGroup();
|
||||
const definitions = await Promise.all(data.map(async (item) => {
|
||||
const response = await getDetect(item.itemId)
|
||||
if (!response.data?.flowData) return []
|
||||
return getResourceRoles(normalizeFlowResourceSchema(JSON.parse(response.data.flowData)))
|
||||
}))
|
||||
roles.value = mergeResourceRoleDefinitions(definitions)
|
||||
}
|
||||
};
|
||||
|
||||
@ -123,12 +125,23 @@ const confirm = () => {
|
||||
ruleFormRef.value
|
||||
.validate()
|
||||
.then(async () => {
|
||||
const runParams = {}
|
||||
const runParams = {}
|
||||
formData.value.tableData.forEach(item => {
|
||||
const data = formatTableData(item.config)
|
||||
runParams[item.itemId] = data
|
||||
})
|
||||
const res = await taskExecute({ runParams, taskId: props.currentRow.id, robotId: formData.value.robotId })
|
||||
const assignmentError = runtimeEditorRef.value?.validate()
|
||||
if (assignmentError) {
|
||||
ElMessage.warning(assignmentError)
|
||||
return
|
||||
}
|
||||
const robotId = runtimeAssignments.value.flatMap(item => item.members).find(member => member.robotId)?.robotId || ''
|
||||
const res = await taskExecute({
|
||||
runParams,
|
||||
taskId: props.currentRow.id,
|
||||
robotId,
|
||||
runtimeAssignments: runtimeAssignments.value
|
||||
})
|
||||
if (res.code === 200) {
|
||||
ruleFormRef.value.resetFields();
|
||||
ElMessage.success('执行成功')
|
||||
@ -142,22 +155,6 @@ const confirm = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const robotIdOptions = ref([]);
|
||||
|
||||
/** 获取设备终端列表 */
|
||||
const handleGetTerminalGroup = async (row) => {
|
||||
const res = await getRobotList({ pageNum: 1, pageSize: 1000, connectStatus: '1' });
|
||||
if (res.code === 200) {
|
||||
robotIdOptions.value = res.rows?.map((item) => {
|
||||
return {
|
||||
label: `${item.robotName || item.robotId} (${item.robotId})`,
|
||||
value: item.robotId,
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
watch(
|
||||
() => props.drawer,
|
||||
(newVal) => {
|
||||
@ -171,3 +168,16 @@ onUnmounted(() => {
|
||||
emitter.off("contentChange");
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.run-section-title {
|
||||
margin: 4px 0 10px;
|
||||
color: var(--el-text-color-primary);
|
||||
font-size: 15px;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.run-section-title--params {
|
||||
margin-top: 22px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -7,16 +7,9 @@
|
||||
:rules="rules"
|
||||
label-width="auto"
|
||||
>
|
||||
<el-form-item label="执行机器人" prop="formData.robotId" :rules="[{ required: true, message: '请选择在线机器人', trigger: 'change' }]">
|
||||
<el-select v-model="formData.robotId" style="width: 260px" placeholder="请选择在线机器人">
|
||||
<el-option
|
||||
v-for="item in robotIdOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<div class="run-section-title">本次执行安排</div>
|
||||
<RuntimeAssignmentsEditor ref="runtimeEditorRef" v-model="runtimeAssignments" :roles="roles" />
|
||||
<div class="run-section-title run-section-title--params">运行参数</div>
|
||||
<el-table
|
||||
:data="formData.tableData"
|
||||
:border="parentBorder"
|
||||
@ -71,11 +64,13 @@
|
||||
</el-drawer>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, watch, onUnmounted } from "vue";
|
||||
import { ref, watch } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { addValueProperty, formatTableData } from "@/utils/flow";
|
||||
import { getRobotList } from '@/api/inspection/robot'
|
||||
import { getExecuteRunParams, projectExecute } from "@/api/vi/project.js"
|
||||
import { getDetect } from '@/api/test/detect'
|
||||
import RuntimeAssignmentsEditor from '@/views/flow/components/RuntimeAssignmentsEditor.vue'
|
||||
import { getResourceRoles, mergeResourceRoleDefinitions, normalizeFlowResourceSchema } from '@/utils/flowResources'
|
||||
|
||||
const props = defineProps({
|
||||
drawer: Boolean,
|
||||
@ -88,32 +83,24 @@ const ruleFormRef = ref();
|
||||
const rules = ref({});
|
||||
|
||||
const formData = ref({
|
||||
robotId: '',
|
||||
tableData: []
|
||||
})
|
||||
|
||||
const robotIdOptions = ref([]);
|
||||
|
||||
/** 获取设备终端列表 */
|
||||
const handleGetTerminalGroup = async (row) => {
|
||||
const res = await getRobotList({ pageNum: 1, pageSize: 1000, connectStatus: '1' });
|
||||
if (res.code === 200) {
|
||||
robotIdOptions.value = res.rows?.map((item) => {
|
||||
return {
|
||||
label: `${item.robotName || item.robotId} (${item.robotId})`,
|
||||
value: item.robotId,
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
const roles = ref([])
|
||||
const runtimeAssignments = ref([])
|
||||
const runtimeEditorRef = ref()
|
||||
|
||||
const getProjectParams = async () => {
|
||||
const res = await getExecuteRunParams(props.projectId);
|
||||
if (res.code === 200) {
|
||||
const data = parseConfig(res.data)
|
||||
console.log('data', data)
|
||||
formData.value.tableData = data
|
||||
handleGetTerminalGroup();
|
||||
const definitions = await Promise.all(data.map(async (item) => {
|
||||
const response = await getDetect(item.itemId)
|
||||
return response.data?.flowData
|
||||
? getResourceRoles(normalizeFlowResourceSchema(JSON.parse(response.data.flowData)))
|
||||
: []
|
||||
}))
|
||||
roles.value = mergeResourceRoleDefinitions(definitions)
|
||||
}
|
||||
};
|
||||
|
||||
@ -137,7 +124,18 @@ const confirm = () => {
|
||||
const data = formatTableData(item.config)
|
||||
runParams[item.itemId] = data
|
||||
})
|
||||
const res = await projectExecute({ runParams, projectId: props.projectId, robotId: formData.value.robotId })
|
||||
const assignmentError = runtimeEditorRef.value?.validate()
|
||||
if (assignmentError) {
|
||||
ElMessage.warning(assignmentError)
|
||||
return
|
||||
}
|
||||
const robotId = runtimeAssignments.value.flatMap(item => item.members).find(member => member.robotId)?.robotId || ''
|
||||
const res = await projectExecute({
|
||||
runParams,
|
||||
projectId: props.projectId,
|
||||
robotId,
|
||||
runtimeAssignments: runtimeAssignments.value
|
||||
})
|
||||
if (res.code === 200) {
|
||||
ruleFormRef.value.resetFields();
|
||||
ElMessage.success('执行成功')
|
||||
@ -165,3 +163,8 @@ watch(
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.run-section-title { margin: 4px 0 10px; font-size: 15px; font-weight: 650; }
|
||||
.run-section-title--params { margin-top: 22px; }
|
||||
</style>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user