feat(flow): 重构流程节点设备绑定机制

- 移除原有的 deviceId 参数绑定方式,改用统一的执行目标选择器
- 添加 RobotExecutionTarget 组件用于机器人角色和设备用途选择
- 更新节点参数验证逻辑,确保执行目标正确配置
- 优化流程资源管理,添加角色和设备槽位配置功能
- 改进复制粘贴功能,支持跨窗口节点复制和引用处理
- 重构设备选择对话框,集成到参数表单中统一管理
- 更新流程节点渲染,显示执行角色标签
- 添加流程资源验证和规范化处理逻辑
This commit is contained in:
lixiaolong 2026-08-11 16:50:30 +08:00
parent 40bd866f91
commit 0df5280ebf
14 changed files with 1005 additions and 850 deletions

View File

@ -38,10 +38,11 @@ export function deleteRunTask(id) {
}) })
} }
export function startRunTask(id) { export function startRunTask(id, data) {
return request({ return request({
url: `/inspection/taskInstance/start/${id}`, url: `/inspection/taskInstance/start/${id}`,
method: 'put' method: 'put',
data
}) })
} }

View File

@ -32,10 +32,11 @@ export function deleteTaskinstance(ids) {
}) })
} }
export function startTaskinstance(id) { export function startTaskinstance(id, data) {
return request({ return request({
url: `/aima/taskinstance/start/${id}`, url: `/aima/taskinstance/start/${id}`,
method: 'put' method: 'put',
data
}) })
} }

View File

@ -1,328 +1,319 @@
<!-- <!-- 流程试运行:分配机器人角色并收集业务运行参数。 -->
* 文件说明:流程试运行抽屉,收集运行参数并绑定机器人设备。
* 作用范围:仅服务于流程设计器模块。
-->
<template> <template>
<el-drawer <el-drawer :model-value="props.drawer" title="试运行" size="min(620px, 96vw)" :before-close="handleClose">
:model-value="props.drawer" <div class="section-heading">本次执行安排</div>
title="试运行" <div class="assignment-tip">流程只保存角色分工,以下机器人和设备仅用于本次运行。</div>
:before-close="handleClose"
> <div v-for="role in roles" :key="role.roleKey" class="assignment-row">
<el-form <div class="assignment-row__title">
ref="ruleFormRef" <span>{{ role.displayName }}</span>
style="max-width: 600px" <el-tag v-if="role.required" size="small" type="info">必需</el-tag>
: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>
<div class="binding-panel__text" v-if="bindingLoading">正在读取机器人在线设备...</div> <el-form label-position="top">
<div class="binding-panel__text" v-else-if="bindingSummary.robotId"> <el-form-item label="执行机器人" required>
已匹配 {{ bindingSummary.matched }} 个设备节点 <el-select
<span v-if="bindingSummary.missing">,{{ bindingSummary.missing }} 个节点未匹配并保留原值</span> :model-value="getMember(role).robotId"
<span v-if="bindingSummary.untouched">,{{ bindingSummary.untouched }} 个手动设备节点保持不变</span> filterable
</div> :loading="robotLoading"
<div class="binding-panel__text" v-else>选择机器人后,将按节点类型自动填充该机器人上报的在线设备。</div> placeholder="请选择机器人"
</div> style="width: 100%"
<el-table @change="(robotId) => handleRobotChange(role, robotId)"
:data="tableData" >
style="width: 100%; margin-bottom: 20px" <el-option v-for="item in robotOptions" :key="item.value" :label="item.label" :value="item.value" />
row-key="name" </el-select>
default-expand-all </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-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 <el-option
v-for="item in robotIdOptions" v-for="item in getSlotOptions(role, slot)"
:key="item.value" :key="item.value"
:label="item.label" :label="item.label"
:value="item.value" :value="item.value"
/> />
</el-select> </el-select>
<div v-if="getMember(role).robotId && !getSlotOptions(role, slot).length" class="assignment-error">
该机器人没有匹配的在线设备
</div>
</el-form-item> </el-form-item>
</el-form>
</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' }] : []"> <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 v-if="row.type === 'string'" v-model="row.value" />
<el-input-number v-if="row.type === 'number'" :controls="false" v-model="row.value" /> <el-input-number v-else-if="row.type === 'number'" v-model="row.value" :controls="false" />
<el-switch v-if="row.type === 'boolean'" v-model="row.value" /> <el-switch v-else-if="row.type === 'boolean'" v-model="row.value" />
</el-form-item> </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-form-item
<el-input v-model="row.value" placeholder="请输入数组值,格式如:[1,2,3]" /> 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> </el-form-item>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<el-empty v-if="!tableData.length" :image-size="52" description="没有需要填写的运行参数" />
</el-form> </el-form>
<template #footer> <template #footer>
<el-button @click="handleClose">关闭</el-button> <el-button @click="handleClose">关闭</el-button>
<el-button type="primary" :loading="bindingLoading" :disabled="bindingLoading" @click="confirm">确定</el-button> <el-button type="primary" :loading="submitting" @click="confirm">开始运行</el-button>
</template> </template>
</el-drawer> </el-drawer>
</template> </template>
<script setup> <script setup>
import { ref, watch } from 'vue' import { reactive, ref, watch } from 'vue'
import { getStartNodeFormData, formatTableData } from '@/utils/flow' import { ElMessage } from 'element-plus'
// import { Plus, Minus } from '@element-plus/icons-vue'
import { flowExecuteTrial } from '@/api/flow/flow' import { flowExecuteTrial } from '@/api/flow/flow'
import { emitter } from '@/utils/eventBus';
import { ElMessage } from 'element-plus';
import { getRobotList, getRobotDevicesByRobotId } from '@/api/inspection/robot' import { getRobotList, getRobotDevicesByRobotId } from '@/api/inspection/robot'
import { getDeviceKindForAction, toDeviceOptions } from '@/utils/robotDevice' import { emitter } from '@/utils/eventBus'
import { formatTableData, getStartNodeFormData } from '@/utils/flow'
const props = defineProps({ import { toDeviceOptions } from '@/utils/robotDevice'
drawer: Boolean, import {
flowId: String getResourceRoles,
}) normalizeFlowResourceSchema,
sanitizeFlowResourceDefinition,
const ruleFormRef = ref() validateFlowResources,
const rules = ref({}); } from '@/utils/flowResources'
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())
const props = defineProps({ drawer: Boolean, flowId: String })
const emits = defineEmits(['close', 'changeState']) const emits = defineEmits(['close', 'changeState'])
/** const ruleFormRef = ref()
* 克隆 cloneData 对应的数据或交互;作用范围仅限当前组件或模块。 const tableData = ref([])
* @param {*} data 待处理的数据 const roles = ref([])
*/ const runtimeAssignments = ref([])
const cloneData = (data) => JSON.parse(JSON.stringify(data || [])) const robotOptions = ref([])
const devicesByRole = reactive({})
const robotLoading = ref(false)
const loadingRoleKey = ref('')
const submitting = ref(false)
const trialRunCache = new Map()
const clone = (value) => JSON.parse(JSON.stringify(value || []))
const cacheKey = () => props.flowId || '__new_flow__'
// 根据参数层级恢复缓存值,同时兼容流程参数结构发生增删的情况。
/**
* 合并 mergeCachedValues 对应的数据或交互;作用范围仅限当前组件或模块。
* @param {*} source 源数据
* @param {*} cached 缓存数据
*/
const mergeCachedValues = (source, cached) => { const mergeCachedValues = (source, cached) => {
const cachedByName = new Map((cached || []).map(item => [item.name, item])) const cachedByName = new Map((cached || []).map((item) => [item.name, item]))
return (source || []).map((item, index) => { return (source || []).map((item, index) => {
const cachedAtIndex = cached?.[index] const cachedItem = cached?.[index]?.name === item.name ? cached[index] : cachedByName.get(item.name)
const cachedItem = cachedAtIndex?.name === item.name ? cachedAtIndex : cachedByName.get(item.name)
if (!cachedItem) return item if (!cachedItem) return item
const merged = { ...item, value: cachedItem.value } const merged = { ...item, value: cachedItem.value }
if (Array.isArray(item.children)) { if (Array.isArray(item.children)) merged.children = mergeCachedValues(item.children, cachedItem.children)
merged.children = mergeCachedValues(item.children, cachedItem.children)
}
return merged return merged
}) })
} }
/** const getAssignment = (role) => runtimeAssignments.value.find((item) => item.roleKey === role.roleKey)
* 处理 cacheKey 对应的数据或交互;作用范围仅限当前组件或模块。 const getMember = (role) => getAssignment(role).members[0]
*/
const cacheKey = () => props.flowId || '__new_flow__'
// 深拷贝快照,避免后续表单编辑直接修改缓存内容。 const createAssignments = (cached = []) => roles.value.map((role) => {
/** const old = cached.find((item) => item.roleKey === role.roleKey)
* 缓存 rememberTrialRunParams 对应的数据或交互;作用范围仅限当前组件或模块。 return old || {
*/ roleKey: role.roleKey,
const rememberTrialRunParams = () => { members: [{
if (!tableData.value.length) return memberKey: `${role.roleKey}_1`,
trialRunCache.set(cacheKey(), cloneData(tableData.value)) 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),
})
} }
/** const loadRobotOptions = async () => {
* 处理 handleClose 对应的数据或交互;作用范围仅限当前组件或模块。 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 = () => { const handleClose = () => {
rememberTrialRunParams() remember()
ruleFormRef.value?.clearValidate() ruleFormRef.value?.clearValidate()
emits('close') emits('close')
} }
watch(() => props.drawer, const validateAssignments = () => {
(newVal) => { for (const role of roles.value) {
if (newVal) { const member = getMember(role)
const data = getStartNodeFormData() if (role.required && !member.robotId) return `请为“${role.displayName}”选择机器人`
const cached = trialRunCache.get(cacheKey()) for (const slot of role.deviceSlots || []) {
tableData.value = mergeCachedValues(data, cached) if (slot.required && !member.devices[slot.slotKey]) return `请为“${role.displayName}”选择“${slot.displayName}”`
const robotId = tableData.value.find(item => item.name === 'robotId')?.value || ''
bindingSummary.value = emptyBindingSummary(robotId)
loadRobotOptions()
} }
} }
) return ''
/**
* 确认 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([]) 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()
* 处理 handleRobotChange 对应的数据或交互;作用范围仅限当前组件或模块。 submitting.value = true
* @param {*} robotId 机器人 ID
*/
const handleRobotChange = async (robotId) => {
// 先拉取机器人设备,再按每个设备节点声明的设备类型筛选候选项。
// 仅在候选唯一时自动回填;无候选或多候选均交由用户确认,避免绑定错误设备。
bindingSummary.value = emptyBindingSummary(robotId || '')
if (!robotId) return
bindingLoading.value = true
try { try {
const response = await getRobotDevicesByRobotId(robotId) const formData = formatTableData(tableData.value)
const devices = Array.isArray(response.data) ? response.data : [] resourceValidation.graph.nodes.forEach((node) => emitter.emit('contentChange', { id: node.id }))
let matched = 0 const executionDefinition = sanitizeFlowResourceDefinition(globalThis.lf.getGraphData())
let missing = 0 const firstRobotId = runtimeAssignments.value.flatMap((item) => item.members).find((member) => member.robotId)?.robotId || ''
let untouched = 0 const response = await flowExecuteTrial({
const missingNodes = [] flowData: JSON.stringify(executionDefinition),
const { nodes } = lf.getGraphData() itemId: props.flowId,
robotId: firstRobotId,
nodes.forEach((node) => { runtimeAssignments: runtimeAssignments.value,
if (node.properties?.nodeType !== 'EDGE') return runParams: formData,
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
}) })
if (response.code !== 200) {
bindingSummary.value = { robotId, matched, missing, untouched } ElMessage.error(response.msg)
if (missingNodes.length) { return
ElMessage.warning(`以下节点没有匹配的在线设备,已保留原值:${missingNodes.join('、')}`)
} else if (matched > 0) {
ElMessage.success(`已自动匹配 ${matched} 个设备节点`)
} }
} catch (error) { emits('changeState', { type: 'testRunning', instId: response.msg })
bindingSummary.value = emptyBindingSummary(robotId)
console.error('自动匹配机器人设备失败:', error)
ElMessage.error('读取机器人设备失败')
} finally { } finally {
bindingLoading.value = false submitting.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
}
})
}
}
</script> </script>
<style lang="scss" scoped> <style scoped lang="scss">
.binding-panel { .section-heading {
margin-bottom: 16px; margin: 6px 0 10px;
padding: 12px 14px; color: var(--el-text-color-primary);
border: 1px solid #d7e5f7; font-size: 15px;
border-radius: 6px; font-weight: 650;
background: #f5f9ff;
} }
.binding-panel--warning { .section-heading--params {
border-color: #f3d19e; margin-top: 24px;
background: #fdf6ec;
} }
.binding-panel__title { .assignment-tip {
display: flex; margin-bottom: 14px;
align-items: center; padding: 10px 12px;
gap: 8px; border-left: 3px solid var(--el-color-primary);
color: #24364b; background: var(--el-fill-color-light);
font-weight: 600; color: var(--el-text-color-secondary);
}
.binding-panel__dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #409eff;
}
.binding-panel__text {
margin-top: 6px;
color: #64748b;
font-size: 13px; font-size: 13px;
line-height: 20px; line-height: 20px;
} }
.assignment-row {
padding: 14px 0 4px;
border-bottom: 1px solid var(--el-border-color-lighter);
}
.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> </style>

View File

@ -3,6 +3,11 @@
* 作用范围:仅服务于流程设计器模块。 * 作用范围:仅服务于流程设计器模块。
--> -->
<template> <template>
<RobotExecutionTarget
v-if="isRobotNode"
: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"> <el-collapse-item name="1" icon-position="left">
@ -15,7 +20,7 @@
</el-button> </el-button>
</template> </template>
<div class="form__container"> <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" <el-form :inline="true" :model="formData" :rules="rules" ref="dynamicFormRef" label-position="top"
label-width="auto"> label-width="auto">
<div v-for="(property, index) in formData.nodeParams" :key="index"> <div v-for="(property, index) in formData.nodeParams" :key="index">
@ -118,12 +123,14 @@
import { computed, ref, reactive, watch, nextTick } from 'vue' import { computed, ref, reactive, watch, nextTick } from 'vue'
import { Plus, Minus } from '@element-plus/icons-vue' import { Plus, Minus } from '@element-plus/icons-vue'
import FormItemRecursive from '../FormItemRecursive.vue' import FormItemRecursive from '../FormItemRecursive.vue'
import RobotExecutionTarget from './RobotExecutionTarget.vue'
import { getInput } from '@/utils/flow' import { getInput } from '@/utils/flow'
import { useQuote } from './useQuote.js' import { useQuote } from './useQuote.js'
import { getArmStatus, getArmJointState, getAgvStatus, getAgvStations } from '@/api/flow/flow' import { getArmStatus, getArmJointState, getAgvStatus, getAgvStations } from '@/api/flow/flow'
import { getRobotList, getRobotDevicesByRobotId } from '@/api/inspection/robot' import { getRobotList, getRobotDevicesByRobotId } from '@/api/inspection/robot'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { getDeviceKindForAction, toDeviceOptions } from '@/utils/robotDevice' import { getDeviceKindForAction, toDeviceOptions } from '@/utils/robotDevice'
import { actionRequiresDevice } from '@/utils/flowResources'
const props = defineProps({ const props = defineProps({
data: Object data: Object
@ -133,7 +140,8 @@ const emit = defineEmits(['save-success', 'save-error'])
const formData = reactive({ const formData = reactive({
nodeParams: [], nodeParams: [],
outputParams: [] outputParams: [],
executionTarget: {}
}) })
const rules = reactive({}) const rules = reactive({})
@ -197,6 +205,7 @@ const initData = () => {
const outputParams = JSON.parse(JSON.stringify(props.data.properties.outputParams || [])) const outputParams = JSON.parse(JSON.stringify(props.data.properties.outputParams || []))
formData.nodeParams = nodeParams; formData.nodeParams = nodeParams;
formData.outputParams = outputParams; formData.outputParams = outputParams;
formData.executionTarget = JSON.parse(JSON.stringify(props.data.properties.executionTarget || {}));
} }
watch( watch(
@ -220,6 +229,13 @@ const validateAndSave = async () => {
if (outputFormRef.value) { if (outputFormRef.value) {
await outputFormRef.value.validate((valid) => { if (!valid) outputValid = false }) 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
lf.setProperties(props.data.id, { lf.setProperties(props.data.id, {
@ -290,10 +306,9 @@ const testRule = (property) => {
return [ return [
{ required: true, message: '请从站点列表中选择站点', trigger: 'change' }, { required: true, message: '请从站点列表中选择站点', trigger: 'change' },
{ validator: () => { { validator: () => {
const deviceId = formData.nodeParams.find(param => param.name === 'deviceId')?.input
const selectedFromList = property.selectOptions?.some(option => option.value === property.input) const selectedFromList = property.selectOptions?.some(option => option.value === property.input)
if (selectedFromList && property.stationDeviceId === deviceId) return Promise.resolve() if (selectedFromList) return Promise.resolve()
return Promise.reject(new Error('设备已变化,请重新加载站点列表')) return Promise.reject(new Error('请重新加载并选择站点'))
}, trigger: 'change' } }, trigger: 'change' }
] ]
} }
@ -331,10 +346,11 @@ const testRule = (property) => {
const robotFormAction = ref() const robotFormAction = ref()
const stationListLoading = ref(false) const stationListLoading = ref(false)
const POSITION_ACTIONS = ['AGV_MOVE_TO_POINT', 'ARM_MOVE_TO_POINT', 'ARM_MOVE_TO_J'] const POSITION_ACTIONS = ['AGV_MOVE_TO_POINT', 'ARM_MOVE_TO_POINT', 'ARM_MOVE_TO_J']
const hasBindableDevice = computed(() => const isRobotNode = computed(() => props.data.properties?.nodeType === 'EDGE')
formData.nodeParams.some(param => param.name === 'deviceId') const hasDeviceDebugAction = computed(() => (
&& Boolean(getDeviceKindForAction(props.data.properties.action)) POSITION_ACTIONS.includes(props.data.properties.action)
) || props.data.properties.action === 'AGV_MOVE_TO_STATION'
))
const robotActionButtonText = computed(() => { const robotActionButtonText = computed(() => {
if (props.data.properties.action === 'AGV_MOVE_TO_STATION') return '选择机器人并加载站点' if (props.data.properties.action === 'AGV_MOVE_TO_STATION') return '选择机器人并加载站点'
if (POSITION_ACTIONS.includes(props.data.properties.action)) return '选择机器人并获取位置' if (POSITION_ACTIONS.includes(props.data.properties.action)) return '选择机器人并获取位置'
@ -361,8 +377,7 @@ const submitRobotForm = () => {
const testMap = { const testMap = {
x: x, x: x,
y: y, y: y,
theta: theta, theta: theta
deviceId: robotForm.deviceId
} }
// 将获取到的位置信息设置到 formData 中 // 将获取到的位置信息设置到 formData 中
formData.nodeParams.forEach(param => { formData.nodeParams.forEach(param => {
@ -390,15 +405,12 @@ const submitRobotForm = () => {
throw new Error('AGV未返回可用站点') throw new Error('AGV未返回可用站点')
} }
const stationParam = formData.nodeParams.find(param => param.name === 'stationId') const stationParam = formData.nodeParams.find(param => param.name === 'stationId')
const deviceParam = formData.nodeParams.find(param => param.name === 'deviceId')
if (stationParam) { if (stationParam) {
stationParam.selectOptions = stationOptions stationParam.selectOptions = stationOptions
stationParam.stationDeviceId = robotForm.deviceId
if (!stationOptions.some(item => item.value === stationParam.input)) { if (!stationOptions.some(item => item.value === stationParam.input)) {
stationParam.input = '' stationParam.input = ''
} }
} }
if (deviceParam) deviceParam.input = robotForm.deviceId
dialogVisible.value = false dialogVisible.value = false
ElMessage.success(`已加载${stationOptions.length}个AGV站点`) ElMessage.success(`已加载${stationOptions.length}个AGV站点`)
} catch (error) { } catch (error) {
@ -424,7 +436,6 @@ const submitRobotForm = () => {
rx: rx, rx: rx,
ry: ry, ry: ry,
rz: rz, rz: rz,
deviceId: robotForm.deviceId
} }
// 将获取到的位置信息设置到 formData 中 // 将获取到的位置信息设置到 formData 中
formData.nodeParams.forEach(param => { formData.nodeParams.forEach(param => {
@ -453,28 +464,13 @@ const submitRobotForm = () => {
} }
dialogVisible.value = false dialogVisible.value = false
const targetParam = formData.nodeParams.find(param => param.name === 'target') 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 (targetParam) targetParam.input = JSON.stringify(target)
if (deviceParam) deviceParam.input = robotForm.deviceId
} }
} catch (error) { } catch (error) {
console.error('获取机械臂关节位置失败:', 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) => { const openRobotForm = async (action) => {
dialogVisible.value = true dialogVisible.value = true
robotFormAction.value = action robotFormAction.value = action
const currentDeviceId = formData.nodeParams.find(param => param.name === 'deviceId')?.input || '' robotForm.deviceId = ''
robotForm.deviceId = currentDeviceId
await loadRobotOptions() await loadRobotOptions()
if (robotForm.robotId) await loadRobotDevices(robotForm.robotId) if (robotForm.robotId) await loadRobotDevices(robotForm.robotId)
} }

View File

@ -3,6 +3,7 @@
* 作用范围:仅服务于流程设计器模块。 * 作用范围:仅服务于流程设计器模块。
--> -->
<template> <template>
<RobotExecutionTarget :data="props.data" v-model="formData.executionTarget" />
<el-collapse v-model="activeNames"> <el-collapse v-model="activeNames">
<el-collapse v-model="activeNames"> <el-collapse v-model="activeNames">
<el-collapse-item name="1" icon-position="left"> <el-collapse-item name="1" icon-position="left">
@ -20,15 +21,6 @@
</el-tooltip> </el-tooltip>
</template> </template>
<div class="form__container"> <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 <el-form
:inline="true" :inline="true"
:model="formData" :model="formData"
@ -203,51 +195,16 @@
</el-collapse> </el-collapse>
</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> </template>
<script setup> <script setup>
import { ref, reactive, watch, nextTick, onBeforeUnmount, onMounted } from 'vue' import { ref, reactive, watch, nextTick, onBeforeUnmount, onMounted } from 'vue'
import { Plus, Minus } from '@element-plus/icons-vue' import { Plus, Minus } from '@element-plus/icons-vue'
import FormItemRecursive from '../FormItemRecursive.vue' import FormItemRecursive from '../FormItemRecursive.vue'
import RobotExecutionTarget from './RobotExecutionTarget.vue'
import * as monaco from 'monaco-editor'; import * as monaco from 'monaco-editor';
import { getInput } from '@/utils/flow' import { getInput } from '@/utils/flow'
import { useQuote } from './useQuote.js' import { useQuote } from './useQuote.js'
import { getRobotList, getRobotDevicesByRobotId } from '@/api/inspection/robot'
import { toDeviceOptions } from '@/utils/robotDevice'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
const props = defineProps({ const props = defineProps({
@ -258,7 +215,8 @@ const emit = defineEmits(['save-success', 'save-error'])
const formData = reactive({ const formData = reactive({
nodeParams: [], nodeParams: [],
outputParams: [] outputParams: [],
executionTarget: {}
}) })
const codeRef = ref() const codeRef = ref()
@ -347,83 +305,6 @@ const dynamicFormRef = ref()
const outputFormRef = ref() const outputFormRef = ref()
const activeNames = ref(['1', '2']) 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 逻辑 // 使用共享的 quote 逻辑
const { quoteOptions, cascaderRefs, handleTypeChange, cascaderChange, visibleChange } = useQuote(props.data.id) 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 || [])) const outputParams = JSON.parse(JSON.stringify(props.data.properties.outputParams || []))
formData.nodeParams = nodeParams; formData.nodeParams = nodeParams;
formData.outputParams = outputParams; formData.outputParams = outputParams;
formData.executionTarget = JSON.parse(JSON.stringify(props.data.properties.executionTarget || {}));
const codeParams = props.data.properties.nodeParams.find(item => item.name === 'requestJson') const codeParams = props.data.properties.nodeParams.find(item => item.name === 'requestJson')
if (codeParams) { if (codeParams) {
setTimeout(() => { setTimeout(() => {
@ -512,6 +394,10 @@ const validateAndSave = async () => {
if (outputFormRef.value) { if (outputFormRef.value) {
await outputFormRef.value.validate((valid) => { if (!valid) outputValid = false }) await outputFormRef.value.validate((valid) => { if (!valid) outputValid = false })
} }
if (!formData.executionTarget.roleKey || !formData.executionTarget.deviceSlotKey) {
inputValid = false
ElMessage.warning('请选择执行角色和设备用途')
}
// 保存到 lf // 保存到 lf
lf.setProperties(props.data.id, { 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 { .code__container {
display: flex; display: flex;
position: relative; position: relative;

View File

@ -1,207 +1,227 @@
<!-- <!-- 开始节点维护流程级机器人角色和业务输入参数。 -->
* 文件说明:开始节点参数面板,配置流程入口参数。
* 作用范围:仅服务于流程设计器模块。
-->
<template> <template>
<el-collapse v-model="activeNames"> <el-collapse v-model="activeNames">
<el-collapse-item name="1" icon-position="left"> <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> <template #title>
输入参数 输入参数
<el-button :circle="true" size="small" @click="(e) => addFormItem(e, 'inputParams')" class="addFormItem"> <el-button circle size="small" class="addFormItem" @click="(event) => addFormItem(event, 'inputParams')">
<el-icon :size="14"> <el-icon :size="14"><Plus /></el-icon>
<Plus />
</el-icon>
</el-button> </el-button>
</template> </template>
<el-form :inline="true" :model="formData" :rules="rules" ref="dynamicFormRef" label-position="top" <el-form
label-width="auto"> ref="dynamicFormRef"
<FormItemRecursive formType="input" :current-list="formData.inputParams" prop-path="inputParams" :inline="true"
:depth="0" :is-first-level="true" :endDepth="2" :parent-path="[]" :model="formData"
@delete-item="(path) => deleteTopLevelItem(path, 'inputParams')" /> 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-form>
</el-collapse-item> </el-collapse-item>
</el-collapse> </el-collapse>
</template> </template>
<script setup> <script setup>
import { ref, reactive, watch, nextTick } from 'vue' import { reactive, ref, watch } from 'vue'
import { Plus, Minus } from '@element-plus/icons-vue' import { Delete, Minus, Plus } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import FormItemRecursive from '../FormItemRecursive.vue' import FormItemRecursive from '../FormItemRecursive.vue'
import { getInput } from '@/utils/flow' import {
import { useQuote } from './useQuote.js' createDefaultRobotRole,
createDeviceSlot,
const props = defineProps({ createResourceKey,
data: Object normalizeFlowResourceSchema,
}) } from '@/utils/flowResources'
const props = defineProps({ data: Object })
const emit = defineEmits(['save-success', 'save-error']) const emit = defineEmits(['save-success', 'save-error'])
const formData = reactive({ const deviceKinds = [
inputParams: [], { 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 dynamicFormRef = ref()
const activeNames = ref(['roles', 'inputs'])
const activeNames = ref(['1', '2']) const addRole = () => {
formData.resourceRoles.push({
// 添加表单项 ...createDefaultRobotRole(),
/** roleKey: createResourceKey('robot_role'),
* 新增 addFormItem 对应的数据或交互;作用范围仅限当前组件或模块。 displayName: `机器人角色${formData.resourceRoles.length + 1}`,
* @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 addDeviceSlot = (role) => {
/** role.deviceSlots.push(createDeviceSlot(null, role.deviceSlots))
* 处理 handleDelete 对应的数据或交互;作用范围仅限当前组件或模块。 }
* @param {*} index 目标项索引
*/ const referencedNodes = (predicate) => globalThis.lf.getGraphData().nodes.filter((node) => predicate(node.properties?.executionTarget))
const handleDelete = (index) => {
formData.nodeParams.splice(index, 1) 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) => { const deleteTopLevelItem = (fullPath, propPath) => {
let currentLevel = formData[propPath] let currentLevel = formData[propPath]
for (let i = 0; i < fullPath.length - 1; i++) { for (let index = 0; index < fullPath.length - 1; index += 1) {
currentLevel = currentLevel[fullPath[i]].children currentLevel = currentLevel[fullPath[index]].children
} }
const lastIndex = fullPath[fullPath.length - 1] currentLevel.splice(fullPath[fullPath.length - 1], 1)
currentLevel.splice(lastIndex, 1)
} }
// 初始化数据
/**
* 初始化 initData 对应的数据或交互;作用范围仅限当前组件或模块。
*/
const initData = () => { const initData = () => {
const inputParams = JSON.parse(JSON.stringify(props.data.properties.inputParams || [])) const graph = normalizeFlowResourceSchema(globalThis.lf.getGraphData())
formData.inputParams = inputParams; 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( watch(() => props.data.id, initData, { immediate: true })
() => props.data,
() => {
initData()
},
{ immediate: true, deep: true }
)
// 保存验证
/**
* 校验 validateAndSave 对应的数据或交互;作用范围仅限当前组件或模块。
*/
const validateAndSave = async () => { const validateAndSave = async () => {
const names = formData.resourceRoles.map((role) => role.displayName.trim())
const invalidRole = formData.resourceRoles.some((role) => !role.displayName.trim())
const duplicateRole = new Set(names).size !== names.length
const invalidSlot = formData.resourceRoles.some((role) => role.deviceSlots.some((slot) => (
!slot.displayName.trim() || slot.deviceKind == null
)))
if (invalidRole || duplicateRole || invalidSlot) {
ElMessage.warning(invalidRole ? '请填写机器人角色名称' : duplicateRole ? '机器人角色名称不能重复' : '请完整填写设备用途和类型')
emit('save-error')
return
}
let inputValid = true let inputValid = true
if (dynamicFormRef.value) { if (dynamicFormRef.value) {
await dynamicFormRef.value.validate((valid) => { if (!valid) inputValid = false }) await dynamicFormRef.value.validate((valid) => { if (!valid) inputValid = false })
} }
globalThis.lf.setProperties(props.data.id, {
// 保存到 lf
lf.setProperties(props.data.id, {
...props.data.properties, ...props.data.properties,
...formData schemaVersion: 2,
resourceRoles: JSON.parse(JSON.stringify(formData.resourceRoles)),
inputParams: JSON.parse(JSON.stringify(formData.inputParams)),
}) })
if (inputValid) { emit(inputValid ? 'save-success' : 'save-error')
emit('save-success')
} else {
emit('save-error')
}
} }
defineExpose({ validateAndSave }) defineExpose({ validateAndSave })
</script> </script>
<style lang="scss" scoped> <style scoped lang="scss">
.form__container { .role-intro {
margin: 12px 0; margin-bottom: 14px;
color: var(--el-text-color-secondary);
:deep(.el-row) { font-size: 13px;
align-items: end; line-height: 20px;
.el-form-item {
margin-right: 12px;
} }
.param-name { .role-row {
width: 160px; margin-bottom: 14px;
padding: 14px;
border: 1px solid var(--el-border-color-light);
border-radius: 6px;
} }
.param-type { .role-row__header,
width: 80px; .device-row {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 10px;
align-items: center;
} }
.param-value { .role-row__section {
width: 160px; margin-top: 14px;
padding-top: 12px;
border-top: 1px solid var(--el-border-color-lighter);
} }
.role-row__label {
margin-bottom: 8px;
color: var(--el-text-color-regular);
font-size: 13px;
font-weight: 600;
} }
.device-row {
grid-template-columns: minmax(0, 1fr) 132px auto;
.sub-properties { margin-bottom: 8px;
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 { .empty-tip {
margin-left: 10px; margin-bottom: 6px;
color: var(--el-text-color-placeholder);
.zw { font-size: 12px;
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;
}
} }
</style> </style>

View File

@ -107,9 +107,7 @@ export const collapseList = [
desc: "在获取相机照片时,需要先启动相机", desc: "在获取相机照片时,需要先启动相机",
action: 'CAMERA_START', action: 'CAMERA_START',
nodeType: 'EDGE', nodeType: 'EDGE',
nodeParams: [ nodeParams: [],
{ name: "deviceId", type: "input", input: "", componentType: 'select', selectOptions: [], disabled: true }
],
outputParams: [], outputParams: [],
outputType: 'json' outputType: 'json'
}, },
@ -120,9 +118,7 @@ export const collapseList = [
desc: "获取相机拍摄的照片", desc: "获取相机拍摄的照片",
action: 'CAMERA_GETRGBIMAGE', action: 'CAMERA_GETRGBIMAGE',
nodeType: 'EDGE', nodeType: 'EDGE',
nodeParams: [ nodeParams: [],
{ name: "deviceId", type: "input", input: "", componentType: 'select', selectOptions: [], disabled: true }
],
outputType: 'img', outputType: 'img',
outputParams: [{ name: 'imageUrl', type: 'array<string>', desc: '图片地址数组', children: [], disabled: true }] outputParams: [{ name: 'imageUrl', type: 'array<string>', desc: '图片地址数组', children: [], disabled: true }]
}, },
@ -133,9 +129,7 @@ export const collapseList = [
desc: "获取相机照片后,关闭该相机", desc: "获取相机照片后,关闭该相机",
action: 'CAMERA_STOP', action: 'CAMERA_STOP',
nodeType: 'EDGE', nodeType: 'EDGE',
nodeParams: [ nodeParams: [],
{ name: "deviceId", type: "input", input: "", componentType: 'select', selectOptions: [], disabled: true }
],
outputParams: [], outputParams: [],
outputType: 'json' outputType: 'json'
}, },
@ -151,9 +145,7 @@ export const collapseList = [
desc: "开启摄像头,并开始录像", desc: "开启摄像头,并开始录像",
action: 'CAMERA_RECORDING_START', action: 'CAMERA_RECORDING_START',
nodeType: 'EDGE', nodeType: 'EDGE',
nodeParams: [ nodeParams: [],
{ name: "deviceId", type: "input", input: "", componentType: 'select', selectOptions: [], disabled: true }
],
outputParams: [], outputParams: [],
outputType: 'json' outputType: 'json'
}, },
@ -164,9 +156,7 @@ export const collapseList = [
desc: "停止录像,并关闭摄像头", desc: "停止录像,并关闭摄像头",
action: 'CAMERA_RECORDING_STOP', action: 'CAMERA_RECORDING_STOP',
nodeType: 'EDGE', nodeType: 'EDGE',
nodeParams: [ nodeParams: [],
{ name: "deviceId", type: "input", input: "", componentType: 'select', selectOptions: [], disabled: true }
],
outputType: 'video', outputType: 'video',
outputParams: [{ name: 'videoUrl', type: 'array<string>', children: [], desc: '视频播放地址数组', disabled: true}] outputParams: [{ name: 'videoUrl', type: 'array<string>', children: [], desc: '视频播放地址数组', disabled: true}]
} }
@ -314,7 +304,6 @@ function handler(params) {
action: 'AGV_MOVE_TO_POINT', action: 'AGV_MOVE_TO_POINT',
nodeType: 'EDGE', nodeType: 'EDGE',
nodeParams: [ nodeParams: [
{ name: "deviceId", type: "input", input: "", disabled: true },
{ name: "x", type: "input", input: "", disabled: true }, { name: "x", type: "input", input: "", disabled: true },
{ name: "y", type: "input", input: "", disabled: true }, { name: "y", type: "input", input: "", disabled: true },
{ name: "theta", type: "input", input: "", disabled: true }, { name: "theta", type: "input", input: "", disabled: true },
@ -329,7 +318,6 @@ function handler(params) {
action: 'AGV_MOVE_TO_STATION', action: 'AGV_MOVE_TO_STATION',
nodeType: 'EDGE', nodeType: 'EDGE',
nodeParams: [ nodeParams: [
{ name: "deviceId", type: "input", input: "", disabled: true },
{ name: "stationId", type: "input", input: "", componentType: 'select', selectOptions: [], forceSelect: true, disabled: true } { name: "stationId", type: "input", input: "", componentType: 'select', selectOptions: [], forceSelect: true, disabled: true }
], ],
outputType: 'json' outputType: 'json'
@ -342,7 +330,6 @@ function handler(params) {
action: 'DEVICE_EXECUTE_JSON_COMMAND', action: 'DEVICE_EXECUTE_JSON_COMMAND',
nodeType: 'EDGE', nodeType: 'EDGE',
nodeParams: [ nodeParams: [
{ name: "deviceId", type: "input", input: "", componentType: 'select', selectOptions: [], required: true, disabled: true },
{ name: "requestJson", type: "input", input: `{ { name: "requestJson", type: "input", input: `{
"a":123 "a":123
}`, required: false, disabled: true }, }`, required: false, disabled: true },
@ -358,7 +345,6 @@ function handler(params) {
action: 'ARM_MOVE_TO_POINT', action: 'ARM_MOVE_TO_POINT',
nodeType: 'EDGE', nodeType: 'EDGE',
nodeParams: [ nodeParams: [
{ name: "deviceId", type: "input", input: "", disabled: true },
{ name: "x", type: "input", input: "", disabled: true }, { name: "x", type: "input", input: "", disabled: true },
{ name: "y", type: "input", input: "", disabled: true }, { name: "y", type: "input", input: "", disabled: true },
{ name: "z", type: "input", input: "", disabled: true }, { name: "z", type: "input", input: "", disabled: true },
@ -378,7 +364,6 @@ function handler(params) {
action: 'ARM_MOVE_TO_J', action: 'ARM_MOVE_TO_J',
nodeType: 'EDGE', nodeType: 'EDGE',
nodeParams: [ nodeParams: [
{ name: "deviceId", type: "input", input: "", disabled: true },
{ name: "target", type: "input", input: "[0, 0, 0, 0, 0, 0]", 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: "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 }, { name: "acceleration", type: "input", componentType: 'number', max: 5, step: 0.01, input: 0.2, disabled: true },
@ -457,9 +442,7 @@ function handler(params) {
desc: "用于启动麦克风的节点", desc: "用于启动麦克风的节点",
action: 'MICROPHONE_START', action: 'MICROPHONE_START',
nodeType: 'EDGE', nodeType: 'EDGE',
nodeParams: [ nodeParams: [],
{ name: "deviceId", type: "input", input: "", componentType: 'select', selectOptions: [], disabled: true }
],
outputType: 'json' outputType: 'json'
}, },
{ {
@ -469,9 +452,7 @@ function handler(params) {
desc: "用于停止麦克风的节点", desc: "用于停止麦克风的节点",
action: 'MICROPHONE_STOP', action: 'MICROPHONE_STOP',
nodeType: 'EDGE', nodeType: 'EDGE',
nodeParams: [ nodeParams: [],
{ name: "deviceId", type: "input", input: "", componentType: 'select', selectOptions: [], disabled: true }
],
outputType: 'json' outputType: 'json'
}, },
{ {
@ -482,7 +463,6 @@ function handler(params) {
action: 'SPEAKER_PLAYAUDIO', action: 'SPEAKER_PLAYAUDIO',
nodeType: 'EDGE', nodeType: 'EDGE',
nodeParams: [ nodeParams: [
{ name: "deviceId", type: "input", input: "", componentType: 'select', selectOptions: [], disabled: true },
{ name: "audioPath", type: "input", input: "", disabled: true } { name: "audioPath", type: "input", input: "", disabled: true }
], ],
outputType: 'json' outputType: 'json'
@ -636,13 +616,12 @@ function handler(params) {
icon: audioSvg, icon: audioSvg,
name: "播放语料", name: "播放语料",
type: "serviceNode", type: "serviceNode",
desc: "用于播放语料的节点,需要输入语音路径(audioPath)和播放语音的设备id(deviceId)", desc: "用于播放语料的节点,需要输入语音路径,执行设备由流程角色配置",
action: 'VI_PLAY_CORPUS', action: 'VI_PLAY_CORPUS',
nodeType: "EDGE", nodeType: "EDGE",
outputType: 'json', outputType: 'json',
nodeParams: [ nodeParams: [
{ name: 'audioPath', type: "input", input: "", disabled: true }, { name: 'audioPath', type: "input", input: "", disabled: true },
{ name: 'deviceId', type: "input", input: "", disabled: true },
{ name: 'bioId', type: "input", input: "", disabled: true, required: false } { name: 'bioId', type: "input", input: "", disabled: true, required: false }
], ],
} }
@ -659,7 +638,6 @@ function handler(params) {
action: 'TOUCH', action: 'TOUCH',
nodeType: "EDGE", nodeType: "EDGE",
nodeParams: [ nodeParams: [
{ name: "deviceId", type: "input", input: "", disabled: true },
{ name: "touchParams", type: "input", input: "", disabled: true } { name: "touchParams", type: "input", input: "", disabled: true }
], ],
outputType: 'json', outputType: 'json',
@ -671,9 +649,7 @@ function handler(params) {
desc: "控制机器人说话", desc: "控制机器人说话",
action: 'BIO_HEAD_SPEAK_START', action: 'BIO_HEAD_SPEAK_START',
nodeType: "EDGE", nodeType: "EDGE",
nodeParams: [ nodeParams: [],
{ name: "deviceId", type: "input", input: "", disabled: true }
],
outputType: 'json', outputType: 'json',
}, },
{ {
@ -683,9 +659,7 @@ function handler(params) {
desc: "停止机器人说话", desc: "停止机器人说话",
action: 'BIO_HEAD_SPEAK_STOP', action: 'BIO_HEAD_SPEAK_STOP',
nodeType: "EDGE", nodeType: "EDGE",
nodeParams: [ nodeParams: [],
{ name: "deviceId", type: "input", input: "", disabled: true }
],
outputType: 'json', outputType: 'json',
}, },
{ {
@ -696,7 +670,6 @@ function handler(params) {
action: 'BIO_HEAD_SPECIAL_EXPRESSION', action: 'BIO_HEAD_SPECIAL_EXPRESSION',
nodeType: "EDGE", nodeType: "EDGE",
nodeParams: [ nodeParams: [
{ name: "deviceId", type: "input", input: "", disabled: true },
{ name: "expressKey", type: "input",componentType: 'select', selectOptions: expressOptions(), disabled: true }, { name: "expressKey", type: "input",componentType: 'select', selectOptions: expressOptions(), disabled: true },
], ],
outputType: 'json', outputType: 'json',

View File

@ -268,6 +268,15 @@ import { ElMessage } from "element-plus";
import { useFlowStore } from "@/store/modules/flow"; import { useFlowStore } from "@/store/modules/flow";
import { emitter } from "@/utils/eventBus"; import { emitter } from "@/utils/eventBus";
import { convertToTree } from "@/utils/flow"; import { convertToTree } from "@/utils/flow";
import {
actionRequiresDevice,
createDefaultRobotRole,
ensureNodeExecutionResources,
getResourceRoles,
mergeCopiedResourceRoles,
normalizeFlowResourceSchema,
validateFlowResources,
} from "@/utils/flowResources";
import { import {
flowDeploy, flowDeploy,
flowView, flowView,
@ -349,6 +358,13 @@ const drop = (e) => {
...other, ...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)) { if (loopNode && !["start", "end", "loop", "selectArea"].includes(newNode.type)) {
lf.addToGroup(loopNode.id, newNode.id, point.canvasOverlayPosition); lf.addToGroup(loopNode.id, newNode.id, point.canvasOverlayPosition);
} }
@ -357,7 +373,7 @@ const drop = (e) => {
!["selectArea", "stopLoop", "subStart", "subEnd"].includes(newNode.type) !["selectArea", "stopLoop", "subStart", "subEnd"].includes(newNode.type)
) { ) {
showParamsDrawer.value = true; 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; 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; isOpen.value = true;
}; };
@ -649,9 +676,9 @@ const initFlowData = {
properties: { properties: {
name: "Start", name: "Start",
action: "start", action: "start",
inputParams: [ schemaVersion: 2,
{ name: "robotId", type: "string", input: "", disabled: true } resourceRoles: [createDefaultRobotRole()],
], inputParams: [],
}, },
}, },
{ {
@ -824,7 +851,7 @@ const loadFlowData = async (id, lf) => {
try { try {
const res = await getDetect(id); const res = await getDetect(id);
if (res.data && res.data.flowData) { if (res.data && res.data.flowData) {
const flowData = JSON.parse(res.data.flowData); const flowData = normalizeFlowResourceSchema(JSON.parse(res.data.flowData));
lf.render(flowData); lf.render(flowData);
nextTick(() => { nextTick(() => {
@ -1075,19 +1102,9 @@ const handleSingleNodeRobotChange = async (robotId) => {
} }
deviceParam.componentType = "select"; deviceParam.componentType = "select";
deviceParam.selectOptions = toDeviceOptions(devices); deviceParam.selectOptions = toDeviceOptions(devices);
if (deviceKind) {
const currentDevice = devices.find((device) => device.deviceId === originalDeviceId); const currentDevice = devices.find((device) => device.deviceId === originalDeviceId);
deviceParam.input = (currentDevice || devices[0]).deviceId; deviceParam.input = (currentDevice || devices[0]).deviceId;
deviceParam.deviceLocked = devices.length === 1; 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("已保留通用指令原设备;该设备未出现在当前机器人的在线列表中");
}
} catch (error) { } catch (error) {
deviceParam.input = originalDeviceId; deviceParam.input = originalDeviceId;
deviceParam.selectOptions = originalOptions; deviceParam.selectOptions = originalOptions;
@ -1153,7 +1170,8 @@ const loadSingleNodeRobots = async () => {
const singleNodeExecution = async (e) => { const singleNodeExecution = async (e) => {
rememberSingleNodeRobot(); rememberSingleNodeRobot();
const { name, nodeType, nodeParams, action, nodeId } = e.detail; 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 normalizedAction = String(action || "").toUpperCase();
const effectiveNodeType = nodeType || (getDeviceKindForAction(normalizedAction) ? "EDGE" : ""); const effectiveNodeType = nodeType || (getDeviceKindForAction(normalizedAction) ? "EDGE" : "");
activeSingleNodeId.value = nodeId || `${normalizedAction}:${name || ""}`; activeSingleNodeId.value = nodeId || `${normalizedAction}:${name || ""}`;
@ -1164,7 +1182,7 @@ const singleNodeExecution = async (e) => {
stationLoading.value = false; stationLoading.value = false;
if (effectiveNodeType === "EDGE") { if (effectiveNodeType === "EDGE") {
await loadSingleNodeRobots(); await loadSingleNodeRobots();
formData.nodeParams = [ const routingParams = [
{ {
disabled: true, disabled: true,
input: "", input: "",
@ -1173,8 +1191,18 @@ const singleNodeExecution = async (e) => {
componentType: "select", componentType: "select",
selectOptions: singleNodeRobotOptions.value, 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; const cachedRobotId = singleNodeRobotCache.get(activeSingleNodeId.value) || lastSingleNodeRobotId;
if (cachedRobotId) { if (cachedRobotId) {
formData.nodeParams[0].input = cachedRobotId; formData.nodeParams[0].input = cachedRobotId;
@ -1246,7 +1274,14 @@ const clearRun = () => {
const NODE_PASTE_OFFSET = 40; const NODE_PASTE_OFFSET = 40;
const NON_COPYABLE_NODE_TYPES = new Set(["start", "end", "selectArea"]); 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; let pasteCount = 0;
const EDITABLE_SELECTOR = [ const EDITABLE_SELECTOR = [
@ -1289,6 +1324,7 @@ const isTextEditing = (event) => {
const copySelectedNodes = (event) => { const copySelectedNodes = (event) => {
// 复制只保留选中节点及它们之间的内部连线,避免粘贴时重建指向未选节点的关系。 // 复制只保留选中节点及它们之间的内部连线,避免粘贴时重建指向未选节点的关系。
// 节点数据在此深拷贝,后续生成新 ID 时不会反向污染原画布对象。 // 节点数据在此深拷贝,后续生成新 ID 时不会反向污染原画布对象。
if (flowStore.disableForm || isTextEditing(event)) return;
const selectedNodes = lf.getSelectElements().nodes; const selectedNodes = lf.getSelectElements().nodes;
if (!selectedNodes.length) return; if (!selectedNodes.length) return;
@ -1321,9 +1357,79 @@ const copySelectedNodes = (event) => {
copyableIds.has(edge.sourceNodeId) && copyableIds.has(edge.targetNodeId) 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(); 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; 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 {*} idMap 节点 ID 映射
* @param {*} parentKey 递归父键名 * @param {*} parentKey 递归父键名
*/ */
const remapCopiedNodeReferences = (value, idMap, parentKey = "") => { const remapCopiedNodeReferences = (value, idMap, parentKey = "", options = {}) => {
if (Array.isArray(value)) { if (Array.isArray(value)) {
const result = value.map((item) => remapCopiedNodeReferences(item, idMap)); const result = value.map((item) => remapCopiedNodeReferences(item, idMap, "", options));
if (parentKey === "quote" && result.length && idMap.has(result[0])) { if (NODE_REFERENCE_ARRAY_KEYS.has(parentKey) && result.length) {
if (idMap.has(result[0])) {
result[0] = idMap.get(result[0]); result[0] = idMap.get(result[0]);
} else if (options.dropExternalReferences) {
options.removedReferenceCount += 1;
return [];
}
} }
return result; return result;
} }
if (!value || typeof value !== "object") return value; if (!value || typeof value !== "object") return value;
return Object.fromEntries(Object.entries(value).map(([key, item]) => ( 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 对应的数据或交互;作用范围仅限当前组件或模块。 * 粘贴 pasteCopiedNodes 对应的数据或交互;作用范围仅限当前组件或模块。
* @param {*} event 触发操作的浏览器事件 * @param {*} event 触发操作的浏览器事件
@ -1364,26 +1507,42 @@ const remapCopiedNodeReferences = (value, idMap, parentKey = "") => {
const pasteCopiedNodes = (event) => { const pasteCopiedNodes = (event) => {
// 先为全部节点建立旧 ID 到新 ID 的映射,再重建父子引用、锚点和内部连线。 // 先为全部节点建立旧 ID 到新 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(); event.preventDefault();
if (lastPastedClipboardId !== payload.clipboardId) {
pasteCount = 0;
lastPastedClipboardId = payload.clipboardId;
}
pasteCount += 1; pasteCount += 1;
const offset = NODE_PASTE_OFFSET * pasteCount; const translation = resolvePasteTranslation(payload);
const graph = lf.getGraphData(); const graph = lf.getGraphData();
const existingNodeIds = new Set(graph.nodes.map((node) => node.id)); const existingNodeIds = new Set(graph.nodes.map((node) => node.id));
const idMap = new Map(copiedGraph.nodes.map((node) => ( const idMap = new Map(copiedGraph.nodes.map((node) => (
[node.id, uuid().replace(/-/g, "")] [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 pastedNodes = copiedGraph.nodes.map((sourceNode) => {
const newId = idMap.get(sourceNode.id); const newId = idMap.get(sourceNode.id);
const properties = remapCopiedNodeReferences( const properties = remapCopiedNodeReferences(
JSON.parse(JSON.stringify(sourceNode.properties || {})), JSON.parse(JSON.stringify(sourceNode.properties || {})),
idMap, idMap,
"",
referenceOptions,
); );
const parentId = properties.parentId; const parentId = properties.parentId;
if (idMap.has(parentId)) properties.parentId = idMap.get(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)) { if (sourceNode.type === "branch" && Array.isArray(properties.anchor)) {
properties.anchor = properties.anchor.map((anchor) => ({ properties.anchor = properties.anchor.map((anchor) => ({
@ -1398,8 +1557,8 @@ const pasteCopiedNodes = (event) => {
return { return {
...JSON.parse(JSON.stringify(sourceNode)), ...JSON.parse(JSON.stringify(sourceNode)),
id: newId, id: newId,
x: Number(sourceNode.x || 0) + offset, x: Number(sourceNode.x || 0) + translation.x,
y: Number(sourceNode.y || 0) + offset, y: Number(sourceNode.y || 0) + translation.y,
properties, properties,
children, 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({ lf.render({
nodes: [...graph.nodes, ...pastedNodes], nodes: [...graphNodesWithResources, ...pastedNodes],
edges: [...graph.edges, ...pastedEdges], edges: [...graph.edges, ...pastedEdges],
}, { }, {
resetHistory: false, resetHistory: false,
@ -1438,6 +1602,14 @@ const pasteCopiedNodes = (event) => {
nextTick(() => { nextTick(() => {
lf.selectElementsByIds(pastedNodes.map((node) => node.id)); lf.selectElementsByIds(pastedNodes.map((node) => node.id));
lf.markChanged(); 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 isModifierKey = event.ctrlKey || event.metaKey;
const key = event.key.toLowerCase(); 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 (isModifierKey && key === "z") {
if (flowStore.disableForm) return; if (flowStore.disableForm) return;
event.preventDefault(); event.preventDefault();
@ -1496,6 +1659,8 @@ onMounted(() => {
document.addEventListener("singleNodeExecution", singleNodeExecution); document.addEventListener("singleNodeExecution", singleNodeExecution);
registerBeforeUnload(); registerBeforeUnload();
window.addEventListener("keydown", handleKeyboardShortcut); window.addEventListener("keydown", handleKeyboardShortcut);
window.addEventListener("copy", copySelectedNodes);
window.addEventListener("paste", pasteCopiedNodes);
onFlowEvent("openParamsDrawer", (node) => { onFlowEvent("openParamsDrawer", (node) => {
showParamsDrawer.value = true; showParamsDrawer.value = true;
@ -1548,6 +1713,8 @@ onUnmounted(() => {
window.removeEventListener("beforeunload", handleBeforeUnload); window.removeEventListener("beforeunload", handleBeforeUnload);
window.removeEventListener("keydown", handleKeyboardShortcut); window.removeEventListener("keydown", handleKeyboardShortcut);
window.removeEventListener("copy", copySelectedNodes);
window.removeEventListener("paste", pasteCopiedNodes);
document.removeEventListener("singleNodeExecution", singleNodeExecution); document.removeEventListener("singleNodeExecution", singleNodeExecution);
}); });
</script> </script>

View File

@ -32,6 +32,7 @@
@sync-anchors="syncBranchAnchors" @sync-anchors="syncBranchAnchors"
@add-to-group="addToGroup" @add-to-group="addToGroup"
/> />
<div v-if="executionLabel" class="flow-node-executor">{{ executionLabel }}</div>
</div> </div>
<template v-if="flowType === 'branch'"> <template v-if="flowType === 'branch'">
@ -111,6 +112,18 @@ const vueFlow = useVueFlow("main-flow");
const componentRef = ref(); const componentRef = ref();
const flowType = computed(() => props.data.flowType); const flowType = computed(() => props.data.flowType);
const properties = computed(() => props.data.properties || {}); 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 nodeComponent = computed(() => componentMap[flowType.value] || ServiceNode);
const isGroup = computed(() => ["loop", "selectArea"].includes(flowType.value)); const isGroup = computed(() => ["loop", "selectArea"].includes(flowType.value));
const childNodeCount = computed(() => ( const childNodeCount = computed(() => (
@ -285,6 +298,16 @@ onMounted(() => bindComponent(componentRef.value));
box-sizing: border-box; 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 { .flow-node-card {
width: 100%; width: 100%;
height: auto; height: auto;

View File

@ -143,6 +143,14 @@
<el-dialog title="任务执行日志" v-model="logDialogVisible" width="500px" append-to-body> <el-dialog title="任务执行日志" v-model="logDialogVisible" width="500px" append-to-body>
<taskLog :list="logDialogData" :height="300" /> <taskLog :list="logDialogData" :height="300" />
</el-dialog> </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> </div>
</template> </template>
<script setup> <script setup>
@ -157,10 +165,20 @@ import { addRunTask, getRunTaskList, startRunTask, pauseRunTask, stopRunTask, re
import { ElMessage, ElMessageBox } from "element-plus"; import { ElMessage, ElMessageBox } from "element-plus";
import { formatDate } from "@/utils/index"; import { formatDate } from "@/utils/index";
import { debounce } from 'lodash'; // 引入 lodash 的防抖函数 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 topContainerRef = ref();
const containerHeight = useContainerHeight(topContainerRef); const containerHeight = useContainerHeight(topContainerRef);
const socket = inject('ws'); 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 = { const taskStatusMap = {
'0': '已完成', '0': '已完成',
@ -324,11 +342,53 @@ const startTask = async (task) => {
getList(); getList();
} }
} else { } else {
let res = await startRunTask(task.id); const taskDefinition = taskList.value.find(item => item.id === task.taskId)
if (res.code === 200) { if (!taskDefinition?.taskConfigId) {
ElMessage.success("任务已开始"); ElMessage.warning('当前任务没有关联流程编排')
getList(); 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
} }
} }

View File

@ -150,6 +150,15 @@
<el-dialog title="任务执行日志" v-model="logDialogVisible" width="500px" append-to-body> <el-dialog title="任务执行日志" v-model="logDialogVisible" width="500px" append-to-body>
<taskLog :list="logDialogData" :height="300" /> <taskLog :list="logDialogData" :height="300" />
</el-dialog> </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> </div>
</template> </template>
<script setup> <script setup>
@ -174,10 +183,20 @@ import {
import { ElMessage, ElMessageBox } from "element-plus"; import { ElMessage, ElMessageBox } from "element-plus";
import { formatDate } from "@/utils/index"; import { formatDate } from "@/utils/index";
import { debounce } from 'lodash'; // 引入 lodash 的防抖函数 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 topContainerRef = ref();
const containerHeight = useContainerHeight(topContainerRef); const containerHeight = useContainerHeight(topContainerRef);
const socket = inject('ws'); 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 = { const taskStatusMap = {
'0': '已完成', '0': '已完成',
@ -343,11 +362,45 @@ const startTask = async (task) => {
getList(); getList();
} }
} else { } else {
let res = await startTaskinstance(task.id); const taskDefinition = taskList.value.find(item => item.id === task.taskId)
if (res.code === 200) { if (!taskDefinition?.taskConfigId) {
ElMessage.success("任务已开始"); ElMessage.warning('当前任务没有关联流程编排')
getList(); 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(() => {
} }
} }
} }
.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> </style>

View File

@ -7,16 +7,9 @@
:rules="rules" :rules="rules"
label-width="auto" label-width="auto"
> >
<el-form-item label="执行机器人" prop="formData.robotId" :rules="[{ required: true, message: '请选择在线机器人', trigger: 'change' }]"> <div class="run-section-title">本次执行安排</div>
<el-select v-model="formData.robotId" style="width: 260px" placeholder="请选择在线机器人"> <RuntimeAssignmentsEditor ref="runtimeEditorRef" v-model="runtimeAssignments" :roles="roles" />
<el-option <div class="run-section-title run-section-title--params">运行参数</div>
v-for="item in robotIdOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-table <el-table
:data="formData.tableData" :data="formData.tableData"
:border="parentBorder" :border="parentBorder"
@ -74,8 +67,10 @@
import { ref, watch } from "vue"; import { ref, watch } from "vue";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { addValueProperty, formatTableData } from "@/utils/flow"; import { addValueProperty, formatTableData } from "@/utils/flow";
import { getRobotList } from '@/api/inspection/robot' import { getExecuteRunParams, projectExecute } from "@/api/is/project/index.js";
import { getExecuteRunParams, projectExecute, executeProject } 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({ const props = defineProps({
@ -90,31 +85,24 @@ const ruleFormRef = ref();
const rules = ref({}); const rules = ref({});
const formData = ref({ const formData = ref({
robotId: '',
tableData: [] tableData: []
}) })
const roles = ref([])
const robotIdOptions = ref([]); const runtimeAssignments = ref([])
const runtimeEditorRef = 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 getProjectParams = async () => { const getProjectParams = async () => {
const res = await getExecuteRunParams({ projectId: props.projectId }); const res = await getExecuteRunParams({ projectId: props.projectId });
if (res.code === 200) { if (res.code === 200) {
const data = parseConfig(res.data) const data = parseConfig(res.data)
formData.value.tableData = 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) const data = formatTableData(item.config)
runParams[item.itemId] = data runParams[item.itemId] = data
}) })
const res = await projectExecute({ runParams, projectId: props.projectId, robotId: formData.value.robotId }) const assignmentError = runtimeEditorRef.value?.validate()
// 后端临时应付检查添加,后续可删除下一行代码,解开上一行代码注释 if (assignmentError) {
// const res = await executeProject(props.functionIds) 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) { if (res.code === 200) {
ruleFormRef.value.resetFields(); ruleFormRef.value.resetFields();
ElMessage.success('执行成功') ElMessage.success('执行成功')
@ -168,3 +165,8 @@ watch(
} }
); );
</script> </script>
<style scoped>
.run-section-title { margin: 4px 0 10px; font-size: 15px; font-weight: 650; }
.run-section-title--params { margin-top: 22px; }
</style>

View File

@ -7,16 +7,9 @@
:rules="rules" :rules="rules"
label-width="auto" label-width="auto"
> >
<el-form-item label="执行机器人" prop="formData.robotId" :rules="[{ required: true, message: '请选择在线机器人', trigger: 'change' }]"> <div class="run-section-title">本次执行安排</div>
<el-select v-model="formData.robotId" style="width: 260px" placeholder="请选择在线机器人"> <RuntimeAssignmentsEditor ref="runtimeEditorRef" v-model="runtimeAssignments" :roles="roles" />
<el-option <div class="run-section-title run-section-title--params">运行参数</div>
v-for="item in robotIdOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-table <el-table
:data="formData.tableData" :data="formData.tableData"
:border="parentBorder" :border="parentBorder"
@ -74,8 +67,10 @@ import { addValueProperty, formatTableData } from "@/utils/flow";
import { emitter } from "@/utils/eventBus"; import { emitter } from "@/utils/eventBus";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { getFlowParamsApi } from "@/api/device/terminal.js"; import { getFlowParamsApi } from "@/api/device/terminal.js";
import { getRobotList } from '@/api/inspection/robot'
import { taskExecute } from "@/api/test/config" 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({ const props = defineProps({
drawer: Boolean, drawer: Boolean,
@ -88,9 +83,11 @@ const preserveExpanded = ref(false);
const ruleFormRef = ref(); const ruleFormRef = ref();
const rules = ref({}); const rules = ref({});
const formData = ref({ const formData = ref({
robotId: '',
tableData: [] tableData: []
}) })
const roles = ref([])
const runtimeAssignments = ref([])
const runtimeEditorRef = ref()
const emits = defineEmits(["close"]); const emits = defineEmits(["close"]);
@ -104,7 +101,12 @@ const getFlowParams = async () => {
if (res.code === 200) { if (res.code === 200) {
const data = parseConfig(res.rows) const data = parseConfig(res.rows)
formData.value.tableData = data 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)
} }
}; };
@ -128,7 +130,18 @@ const confirm = () => {
const data = formatTableData(item.config) const data = formatTableData(item.config)
runParams[item.itemId] = data 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) { if (res.code === 200) {
ruleFormRef.value.resetFields(); ruleFormRef.value.resetFields();
ElMessage.success('执行成功') 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( watch(
() => props.drawer, () => props.drawer,
(newVal) => { (newVal) => {
@ -171,3 +168,16 @@ onUnmounted(() => {
emitter.off("contentChange"); emitter.off("contentChange");
}); });
</script> </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>

View File

@ -7,16 +7,9 @@
:rules="rules" :rules="rules"
label-width="auto" label-width="auto"
> >
<el-form-item label="执行机器人" prop="formData.robotId" :rules="[{ required: true, message: '请选择在线机器人', trigger: 'change' }]"> <div class="run-section-title">本次执行安排</div>
<el-select v-model="formData.robotId" style="width: 260px" placeholder="请选择在线机器人"> <RuntimeAssignmentsEditor ref="runtimeEditorRef" v-model="runtimeAssignments" :roles="roles" />
<el-option <div class="run-section-title run-section-title--params">运行参数</div>
v-for="item in robotIdOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-table <el-table
:data="formData.tableData" :data="formData.tableData"
:border="parentBorder" :border="parentBorder"
@ -71,11 +64,13 @@
</el-drawer> </el-drawer>
</template> </template>
<script setup> <script setup>
import { ref, watch, onUnmounted } from "vue"; import { ref, watch } from "vue";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { addValueProperty, formatTableData } from "@/utils/flow"; import { addValueProperty, formatTableData } from "@/utils/flow";
import { getRobotList } from '@/api/inspection/robot'
import { getExecuteRunParams, projectExecute } from "@/api/vi/project.js" 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({ const props = defineProps({
drawer: Boolean, drawer: Boolean,
@ -88,32 +83,24 @@ const ruleFormRef = ref();
const rules = ref({}); const rules = ref({});
const formData = ref({ const formData = ref({
robotId: '',
tableData: [] tableData: []
}) })
const roles = ref([])
const robotIdOptions = ref([]); const runtimeAssignments = ref([])
const runtimeEditorRef = 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 getProjectParams = async () => { const getProjectParams = async () => {
const res = await getExecuteRunParams(props.projectId); const res = await getExecuteRunParams(props.projectId);
if (res.code === 200) { if (res.code === 200) {
const data = parseConfig(res.data) const data = parseConfig(res.data)
console.log('data', data)
formData.value.tableData = 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) const data = formatTableData(item.config)
runParams[item.itemId] = data 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) { if (res.code === 200) {
ruleFormRef.value.resetFields(); ruleFormRef.value.resetFields();
ElMessage.success('执行成功') ElMessage.success('执行成功')
@ -165,3 +163,8 @@ watch(
} }
); );
</script> </script>
<style scoped>
.run-section-title { margin: 4px 0 10px; font-size: 15px; font-weight: 650; }
.run-section-title--params { margin-top: 22px; }
</style>