feat(flow): 重构流程节点设备绑定机制
- 移除原有的 deviceId 参数绑定方式,改用统一的执行目标选择器 - 添加 RobotExecutionTarget 组件用于机器人角色和设备用途选择 - 更新节点参数验证逻辑,确保执行目标正确配置 - 优化流程资源管理,添加角色和设备槽位配置功能 - 改进复制粘贴功能,支持跨窗口节点复制和引用处理 - 重构设备选择对话框,集成到参数表单中统一管理 - 更新流程节点渲染,显示执行角色标签 - 添加流程资源验证和规范化处理逻辑
This commit is contained in:
parent
40bd866f91
commit
0df5280ebf
@ -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
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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"
|
|
||||||
>
|
|
||||||
<el-form
|
|
||||||
ref="ruleFormRef"
|
|
||||||
style="max-width: 600px"
|
|
||||||
:model="{ tableData }"
|
|
||||||
:rules="rules"
|
|
||||||
label-width="auto"
|
|
||||||
>
|
|
||||||
<div class="binding-panel" :class="{ 'binding-panel--warning': bindingSummary.missing > 0 }">
|
|
||||||
<div class="binding-panel__title">
|
|
||||||
<span class="binding-panel__dot"></span>
|
|
||||||
机器人设备自动匹配
|
|
||||||
</div>
|
|
||||||
<div class="binding-panel__text" v-if="bindingLoading">正在读取机器人在线设备...</div>
|
|
||||||
<div class="binding-panel__text" v-else-if="bindingSummary.robotId">
|
|
||||||
已匹配 {{ bindingSummary.matched }} 个设备节点
|
|
||||||
<span v-if="bindingSummary.missing">,{{ bindingSummary.missing }} 个节点未匹配并保留原值</span>
|
|
||||||
<span v-if="bindingSummary.untouched">,{{ bindingSummary.untouched }} 个手动设备节点保持不变</span>
|
|
||||||
</div>
|
|
||||||
<div class="binding-panel__text" v-else>选择机器人后,将按节点类型自动填充该机器人上报的在线设备。</div>
|
|
||||||
</div>
|
|
||||||
<el-table
|
|
||||||
:data="tableData"
|
|
||||||
style="width: 100%; margin-bottom: 20px"
|
|
||||||
row-key="name"
|
|
||||||
default-expand-all
|
|
||||||
>
|
|
||||||
<el-table-column prop="name" label="入参名称">
|
|
||||||
<template #default="{row}">
|
|
||||||
<span>{{ row.name }}</span>
|
|
||||||
<span v-if="row.required" style="color: #f56c6c; margin-left: 8px;">*</span>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="type" label="入参类型">
|
|
||||||
<template #default="scope">
|
|
||||||
<el-tag>{{ scope.row.type }}</el-tag>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="value" label="入参值">
|
|
||||||
<template #default="{ row, column, $index }">
|
|
||||||
<el-form-item v-if="row.name === 'robotId'" label="" :prop="`tableData${row.propPath}value`" :rules="row.required ? [{ required: true, message: '请选择执行机器人', trigger: 'change' }] : []">
|
|
||||||
<el-select v-model="row.value" filterable style="width: 100%" @change="handleRobotChange">
|
|
||||||
<el-option
|
|
||||||
v-for="item in robotIdOptions"
|
|
||||||
:key="item.value"
|
|
||||||
:label="item.label"
|
|
||||||
:value="item.value"
|
|
||||||
/>
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-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 v-for="role in roles" :key="role.roleKey" class="assignment-row">
|
||||||
<el-input v-if="row.type === 'string'" v-model="row.value" />
|
<div class="assignment-row__title">
|
||||||
<el-input-number v-if="row.type === 'number'" :controls="false" v-model="row.value" />
|
<span>{{ role.displayName }}</span>
|
||||||
<el-switch v-if="row.type === 'boolean'" v-model="row.value" />
|
<el-tag v-if="role.required" size="small" type="info">必需</el-tag>
|
||||||
</el-form-item>
|
</div>
|
||||||
<el-form-item v-else-if="row.type !== 'object' && row.type.includes('array')" label="" :prop="`tableData${row.propPath}value`" :rules="row.required ? [{ required: true, message: '请输入变量名', trigger: 'blur' }] : []">
|
<el-form label-position="top">
|
||||||
<el-input v-model="row.value" placeholder="请输入数组值,格式如:[1,2,3]" />
|
<el-form-item label="执行机器人" required>
|
||||||
</el-form-item>
|
<el-select
|
||||||
</template>
|
:model-value="getMember(role).robotId"
|
||||||
</el-table-column>
|
filterable
|
||||||
</el-table>
|
:loading="robotLoading"
|
||||||
|
placeholder="请选择机器人"
|
||||||
|
style="width: 100%"
|
||||||
|
@change="(robotId) => handleRobotChange(role, robotId)"
|
||||||
|
>
|
||||||
|
<el-option v-for="item in robotOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-for="slot in role.deviceSlots" :key="slot.slotKey" :label="slot.displayName" :required="slot.required">
|
||||||
|
<el-select
|
||||||
|
v-model="getMember(role).devices[slot.slotKey]"
|
||||||
|
filterable
|
||||||
|
:loading="loadingRoleKey === role.roleKey"
|
||||||
|
:disabled="!getMember(role).robotId"
|
||||||
|
:placeholder="getMember(role).robotId ? '请选择设备' : '请先选择机器人'"
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in getSlotOptions(role, slot)"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
<div v-if="getMember(role).robotId && !getSlotOptions(role, slot).length" class="assignment-error">
|
||||||
|
该机器人没有匹配的在线设备
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section-heading section-heading--params">运行参数</div>
|
||||||
|
<el-form ref="ruleFormRef" :model="{ tableData }" label-width="auto">
|
||||||
|
<el-table :data="tableData" row-key="name" default-expand-all style="width: 100%; margin-bottom: 20px">
|
||||||
|
<el-table-column prop="name" label="参数">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span>{{ row.name }}</span><span v-if="row.required" class="required-mark">*</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="type" label="类型" width="100">
|
||||||
|
<template #default="{ row }"><el-tag>{{ row.type }}</el-tag></template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="value" label="值">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-form-item
|
||||||
|
v-if="row.type !== 'object' && !row.type.includes('array')"
|
||||||
|
:prop="`tableData${row.propPath}value`"
|
||||||
|
:rules="row.required ? [{ required: true, message: '请填写参数', trigger: 'blur' }] : []"
|
||||||
|
>
|
||||||
|
<el-input v-if="row.type === 'string'" v-model="row.value" />
|
||||||
|
<el-input-number v-else-if="row.type === 'number'" v-model="row.value" :controls="false" />
|
||||||
|
<el-switch v-else-if="row.type === 'boolean'" v-model="row.value" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item
|
||||||
|
v-else-if="row.type !== 'object'"
|
||||||
|
:prop="`tableData${row.propPath}value`"
|
||||||
|
:rules="row.required ? [{ required: true, message: '请填写参数', trigger: 'blur' }] : []"
|
||||||
|
>
|
||||||
|
<el-input v-model="row.value" placeholder="数组格式,例如:[1,2,3]" />
|
||||||
|
</el-form-item>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<el-empty v-if="!tableData.length" :image-size="52" description="没有需要填写的运行参数" />
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
|
||||||
<el-button @click="handleClose">关闭</el-button>
|
<template #footer>
|
||||||
<el-button type="primary" :loading="bindingLoading" :disabled="bindingLoading" @click="confirm">确定</el-button>
|
<el-button @click="handleClose">关闭</el-button>
|
||||||
</template>
|
<el-button type="primary" :loading="submitting" @click="confirm">开始运行</el-button>
|
||||||
</el-drawer>
|
</template>
|
||||||
|
</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 || []))
|
||||||
/**
|
|
||||||
* 合并 mergeCachedValues 对应的数据或交互;作用范围仅限当前组件或模块。
|
|
||||||
* @param {*} source 源数据
|
|
||||||
* @param {*} cached 缓存数据
|
|
||||||
*/
|
|
||||||
const mergeCachedValues = (source, cached) => {
|
|
||||||
const cachedByName = new Map((cached || []).map(item => [item.name, item]))
|
|
||||||
return (source || []).map((item, index) => {
|
|
||||||
const cachedAtIndex = cached?.[index]
|
|
||||||
const cachedItem = cachedAtIndex?.name === item.name ? cachedAtIndex : cachedByName.get(item.name)
|
|
||||||
if (!cachedItem) return item
|
|
||||||
const merged = { ...item, value: cachedItem.value }
|
|
||||||
if (Array.isArray(item.children)) {
|
|
||||||
merged.children = mergeCachedValues(item.children, cachedItem.children)
|
|
||||||
}
|
|
||||||
return merged
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理 cacheKey 对应的数据或交互;作用范围仅限当前组件或模块。
|
|
||||||
*/
|
|
||||||
const cacheKey = () => props.flowId || '__new_flow__'
|
const cacheKey = () => props.flowId || '__new_flow__'
|
||||||
|
|
||||||
// 深拷贝快照,避免后续表单编辑直接修改缓存内容。
|
const mergeCachedValues = (source, cached) => {
|
||||||
/**
|
const cachedByName = new Map((cached || []).map((item) => [item.name, item]))
|
||||||
* 缓存 rememberTrialRunParams 对应的数据或交互;作用范围仅限当前组件或模块。
|
return (source || []).map((item, index) => {
|
||||||
*/
|
const cachedItem = cached?.[index]?.name === item.name ? cached[index] : cachedByName.get(item.name)
|
||||||
const rememberTrialRunParams = () => {
|
if (!cachedItem) return item
|
||||||
if (!tableData.value.length) return
|
const merged = { ...item, value: cachedItem.value }
|
||||||
trialRunCache.set(cacheKey(), cloneData(tableData.value))
|
if (Array.isArray(item.children)) merged.children = mergeCachedValues(item.children, cachedItem.children)
|
||||||
|
return merged
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
const getAssignment = (role) => runtimeAssignments.value.find((item) => item.roleKey === role.roleKey)
|
||||||
* 处理 handleClose 对应的数据或交互;作用范围仅限当前组件或模块。
|
const getMember = (role) => getAssignment(role).members[0]
|
||||||
*/
|
|
||||||
const handleClose = () => {
|
const createAssignments = (cached = []) => roles.value.map((role) => {
|
||||||
rememberTrialRunParams()
|
const old = cached.find((item) => item.roleKey === role.roleKey)
|
||||||
ruleFormRef.value?.clearValidate()
|
return old || {
|
||||||
emits('close')
|
roleKey: role.roleKey,
|
||||||
|
members: [{
|
||||||
|
memberKey: `${role.roleKey}_1`,
|
||||||
|
robotId: role.legacyRobotId || '',
|
||||||
|
devices: Object.fromEntries((role.deviceSlots || []).map((slot) => [slot.slotKey, slot.legacyDeviceId || ''])),
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const remember = () => {
|
||||||
|
trialRunCache.set(cacheKey(), {
|
||||||
|
tableData: clone(tableData.value),
|
||||||
|
runtimeAssignments: clone(runtimeAssignments.value),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(() => props.drawer,
|
|
||||||
(newVal) => {
|
|
||||||
if (newVal) {
|
|
||||||
const data = getStartNodeFormData()
|
|
||||||
const cached = trialRunCache.get(cacheKey())
|
|
||||||
tableData.value = mergeCachedValues(data, cached)
|
|
||||||
const robotId = tableData.value.find(item => item.name === 'robotId')?.value || ''
|
|
||||||
bindingSummary.value = emptyBindingSummary(robotId)
|
|
||||||
loadRobotOptions()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 确认 confirm 对应的数据或交互;作用范围仅限当前组件或模块。
|
|
||||||
*/
|
|
||||||
const confirm = () => {
|
|
||||||
rememberTrialRunParams()
|
|
||||||
lf.fitView()
|
|
||||||
ruleFormRef.value.validate().then(async () => {
|
|
||||||
const formData = formatTableData(tableData.value)
|
|
||||||
const { nodes } = lf.getGraphData()
|
|
||||||
nodes.forEach(item => {
|
|
||||||
emitter.emit('contentChange', { id: item.id})
|
|
||||||
})
|
|
||||||
|
|
||||||
const flowData = JSON.stringify(lf.getGraphData())
|
|
||||||
const { robotId, ...runParams } = formData
|
|
||||||
const res = await flowExecuteTrial({
|
|
||||||
flowData,
|
|
||||||
itemId: props.flowId,
|
|
||||||
robotId,
|
|
||||||
runParams
|
|
||||||
})
|
|
||||||
if (res.code === 200) {
|
|
||||||
emits('changeState', {
|
|
||||||
type: 'testRunning',
|
|
||||||
instId: res.msg
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
ElMessage.error(res.msg)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {})
|
|
||||||
}
|
|
||||||
|
|
||||||
const robotIdOptions = ref([])
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理 handleRobotChange 对应的数据或交互;作用范围仅限当前组件或模块。
|
|
||||||
* @param {*} robotId 机器人 ID
|
|
||||||
*/
|
|
||||||
const handleRobotChange = async (robotId) => {
|
|
||||||
// 先拉取机器人设备,再按每个设备节点声明的设备类型筛选候选项。
|
|
||||||
// 仅在候选唯一时自动回填;无候选或多候选均交由用户确认,避免绑定错误设备。
|
|
||||||
bindingSummary.value = emptyBindingSummary(robotId || '')
|
|
||||||
if (!robotId) return
|
|
||||||
bindingLoading.value = true
|
|
||||||
try {
|
|
||||||
const response = await getRobotDevicesByRobotId(robotId)
|
|
||||||
const devices = Array.isArray(response.data) ? response.data : []
|
|
||||||
let matched = 0
|
|
||||||
let missing = 0
|
|
||||||
let untouched = 0
|
|
||||||
const missingNodes = []
|
|
||||||
const { nodes } = lf.getGraphData()
|
|
||||||
|
|
||||||
nodes.forEach((node) => {
|
|
||||||
if (node.properties?.nodeType !== 'EDGE') return
|
|
||||||
const nodeParams = JSON.parse(JSON.stringify(node.properties.nodeParams || []))
|
|
||||||
const robotParam = nodeParams.find(param => param.name === 'robotId')
|
|
||||||
if (robotParam) robotParam.input = robotId
|
|
||||||
|
|
||||||
const deviceParam = nodeParams.find(param => param.name === 'deviceId')
|
|
||||||
if (!deviceParam) {
|
|
||||||
if (robotParam) lf.setProperties(node.id, { ...node.properties, nodeParams })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const kind = getDeviceKindForAction(node.properties.action)
|
|
||||||
if (!kind) {
|
|
||||||
untouched += 1
|
|
||||||
if (robotParam) lf.setProperties(node.id, { ...node.properties, nodeParams })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const candidates = devices.filter(device => Number(device.deviceKind) === kind)
|
|
||||||
if (!candidates.length) {
|
|
||||||
missing += 1
|
|
||||||
missingNodes.push(node.properties.name || node.id)
|
|
||||||
if (robotParam) lf.setProperties(node.id, { ...node.properties, nodeParams })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentCandidate = candidates.find(device => device.deviceId === deviceParam.input)
|
|
||||||
deviceParam.input = (currentCandidate || candidates[0]).deviceId
|
|
||||||
deviceParam.componentType = 'select'
|
|
||||||
deviceParam.selectOptions = toDeviceOptions(candidates)
|
|
||||||
deviceParam.deviceLocked = candidates.length === 1
|
|
||||||
lf.setProperties(node.id, { ...node.properties, nodeParams })
|
|
||||||
matched += 1
|
|
||||||
})
|
|
||||||
|
|
||||||
bindingSummary.value = { robotId, matched, missing, untouched }
|
|
||||||
if (missingNodes.length) {
|
|
||||||
ElMessage.warning(`以下节点没有匹配的在线设备,已保留原值:${missingNodes.join('、')}`)
|
|
||||||
} else if (matched > 0) {
|
|
||||||
ElMessage.success(`已自动匹配 ${matched} 个设备节点`)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
bindingSummary.value = emptyBindingSummary(robotId)
|
|
||||||
console.error('自动匹配机器人设备失败:', error)
|
|
||||||
ElMessage.error('读取机器人设备失败')
|
|
||||||
} finally {
|
|
||||||
bindingLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 获取在线机器人列表 */
|
|
||||||
const loadRobotOptions = async () => {
|
const loadRobotOptions = async () => {
|
||||||
// 试运行不需要要有连接状态为连接的,因为可以选择未连接的机器人进行试运行来保存流程数据
|
robotLoading.value = true
|
||||||
// , connectStatus: '1'
|
try {
|
||||||
const res = await getRobotList({ pageNum: 1, pageSize: 1000 })
|
const response = await getRobotList({ pageNum: 1, pageSize: 1000 })
|
||||||
if (res.code === 200) {
|
robotOptions.value = (response.rows || []).map((robot) => ({
|
||||||
robotIdOptions.value = res.rows?.map((item) => {
|
label: `${robot.robotName || robot.robotId} (${robot.robotId})`,
|
||||||
return {
|
value: robot.robotId,
|
||||||
label: `${item.robotName || item.robotId} (${item.robotId})`,
|
}))
|
||||||
value: item.robotId
|
} finally {
|
||||||
}
|
robotLoading.value = false
|
||||||
})
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getSlotOptions = (role, slot) => toDeviceOptions((devicesByRole[role.roleKey] || []).filter((device) => (
|
||||||
|
slot.deviceKind == null || Number(device.deviceKind) === Number(slot.deviceKind)
|
||||||
|
)))
|
||||||
|
|
||||||
|
const loadRoleDevices = async (role, preserveValues = true) => {
|
||||||
|
const member = getMember(role)
|
||||||
|
if (!member.robotId) {
|
||||||
|
devicesByRole[role.roleKey] = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
loadingRoleKey.value = role.roleKey
|
||||||
|
try {
|
||||||
|
const response = await getRobotDevicesByRobotId(member.robotId)
|
||||||
|
devicesByRole[role.roleKey] = Array.isArray(response.data) ? response.data : []
|
||||||
|
role.deviceSlots.forEach((slot) => {
|
||||||
|
const options = getSlotOptions(role, slot)
|
||||||
|
const currentValid = options.some((option) => option.value === member.devices[slot.slotKey])
|
||||||
|
if (!preserveValues || !currentValid) member.devices[slot.slotKey] = options.length === 1 ? options[0].value : ''
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
devicesByRole[role.roleKey] = []
|
||||||
|
ElMessage.error(`读取“${role.displayName}”的设备失败`)
|
||||||
|
} finally {
|
||||||
|
loadingRoleKey.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRobotChange = async (role, robotId) => {
|
||||||
|
const member = getMember(role)
|
||||||
|
member.robotId = robotId
|
||||||
|
member.devices = Object.fromEntries(role.deviceSlots.map((slot) => [slot.slotKey, '']))
|
||||||
|
await loadRoleDevices(role, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialize = async () => {
|
||||||
|
const graph = normalizeFlowResourceSchema(globalThis.lf.getGraphData())
|
||||||
|
roles.value = getResourceRoles(graph)
|
||||||
|
const cached = trialRunCache.get(cacheKey())
|
||||||
|
tableData.value = mergeCachedValues(getStartNodeFormData(), cached?.tableData)
|
||||||
|
runtimeAssignments.value = createAssignments(cached?.runtimeAssignments)
|
||||||
|
await loadRobotOptions()
|
||||||
|
await Promise.all(roles.value.filter((role) => getMember(role).robotId).map((role) => loadRoleDevices(role)))
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => props.drawer, (open) => { if (open) initialize() })
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
remember()
|
||||||
|
ruleFormRef.value?.clearValidate()
|
||||||
|
emits('close')
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateAssignments = () => {
|
||||||
|
for (const role of roles.value) {
|
||||||
|
const member = getMember(role)
|
||||||
|
if (role.required && !member.robotId) return `请为“${role.displayName}”选择机器人`
|
||||||
|
for (const slot of role.deviceSlots || []) {
|
||||||
|
if (slot.required && !member.devices[slot.slotKey]) return `请为“${role.displayName}”选择“${slot.displayName}”`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirm = async () => {
|
||||||
|
const currentGraph = globalThis.lf.getGraphData()
|
||||||
|
const resourceValidation = validateFlowResources(currentGraph)
|
||||||
|
if (resourceValidation.errors.length) {
|
||||||
|
ElMessage.warning(resourceValidation.errors[0].message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const assignmentError = validateAssignments()
|
||||||
|
if (assignmentError) {
|
||||||
|
ElMessage.warning(assignmentError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const formValid = await ruleFormRef.value?.validate().catch(() => false)
|
||||||
|
if (formValid === false) return
|
||||||
|
|
||||||
|
remember()
|
||||||
|
submitting.value = true
|
||||||
|
try {
|
||||||
|
const formData = formatTableData(tableData.value)
|
||||||
|
resourceValidation.graph.nodes.forEach((node) => emitter.emit('contentChange', { id: node.id }))
|
||||||
|
const executionDefinition = sanitizeFlowResourceDefinition(globalThis.lf.getGraphData())
|
||||||
|
const firstRobotId = runtimeAssignments.value.flatMap((item) => item.members).find((member) => member.robotId)?.robotId || ''
|
||||||
|
const response = await flowExecuteTrial({
|
||||||
|
flowData: JSON.stringify(executionDefinition),
|
||||||
|
itemId: props.flowId,
|
||||||
|
robotId: firstRobotId,
|
||||||
|
runtimeAssignments: runtimeAssignments.value,
|
||||||
|
runParams: formData,
|
||||||
|
})
|
||||||
|
if (response.code !== 200) {
|
||||||
|
ElMessage.error(response.msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
emits('changeState', { type: 'testRunning', instId: response.msg })
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</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);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.binding-panel__dot {
|
.assignment-row {
|
||||||
width: 8px;
|
padding: 14px 0 4px;
|
||||||
height: 8px;
|
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||||
border-radius: 50%;
|
|
||||||
background: #409eff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.binding-panel__text {
|
.assignment-row__title {
|
||||||
margin-top: 6px;
|
display: flex;
|
||||||
color: #64748b;
|
gap: 8px;
|
||||||
font-size: 13px;
|
align-items: center;
|
||||||
line-height: 20px;
|
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>
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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="机器人角色">
|
||||||
<template #title>
|
<div class="role-intro">节点按角色分工,试运行时再安排具体机器人和设备。</div>
|
||||||
输入参数
|
<div v-for="(role, roleIndex) in formData.resourceRoles" :key="role.roleKey" class="role-row">
|
||||||
<el-button :circle="true" size="small" @click="(e) => addFormItem(e, 'inputParams')" class="addFormItem">
|
<div class="role-row__header">
|
||||||
<el-icon :size="14">
|
<el-input v-model="role.displayName" maxlength="30" placeholder="例如:巡检机器人" />
|
||||||
<Plus />
|
<el-button
|
||||||
</el-icon>
|
circle
|
||||||
</el-button>
|
:icon="Delete"
|
||||||
</template>
|
:disabled="formData.resourceRoles.length === 1"
|
||||||
<el-form :inline="true" :model="formData" :rules="rules" ref="dynamicFormRef" label-position="top"
|
aria-label="删除机器人角色"
|
||||||
label-width="auto">
|
@click="deleteRole(roleIndex)"
|
||||||
<FormItemRecursive formType="input" :current-list="formData.inputParams" prop-path="inputParams"
|
/>
|
||||||
:depth="0" :is-first-level="true" :endDepth="2" :parent-path="[]"
|
</div>
|
||||||
@delete-item="(path) => deleteTopLevelItem(path, 'inputParams')" />
|
<div class="role-row__section">
|
||||||
</el-form>
|
<div class="role-row__label">设备用途</div>
|
||||||
</el-collapse-item>
|
<div v-if="!role.deviceSlots.length" class="empty-tip">当前角色暂不需要设备</div>
|
||||||
</el-collapse>
|
<div v-for="(slot, slotIndex) in role.deviceSlots" :key="slot.slotKey" class="device-row">
|
||||||
|
<el-input v-model="slot.displayName" maxlength="30" placeholder="例如:前方摄像头" />
|
||||||
|
<el-select v-model="slot.deviceKind" placeholder="设备类型">
|
||||||
|
<el-option v-for="item in deviceKinds" :key="item.value" :label="item.label" :value="item.value" />
|
||||||
|
</el-select>
|
||||||
|
<el-button circle :icon="Minus" aria-label="删除设备用途" @click="deleteDeviceSlot(role, slotIndex)" />
|
||||||
|
</div>
|
||||||
|
<el-button text type="primary" :icon="Plus" @click="addDeviceSlot(role)">添加设备用途</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-button type="primary" plain :icon="Plus" @click="addRole">添加机器人角色</el-button>
|
||||||
|
</el-collapse-item>
|
||||||
|
|
||||||
|
<el-collapse-item name="inputs">
|
||||||
|
<template #title>
|
||||||
|
输入参数
|
||||||
|
<el-button circle size="small" class="addFormItem" @click="(event) => addFormItem(event, 'inputParams')">
|
||||||
|
<el-icon :size="14"><Plus /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
<el-form
|
||||||
|
ref="dynamicFormRef"
|
||||||
|
:inline="true"
|
||||||
|
:model="formData"
|
||||||
|
label-position="top"
|
||||||
|
label-width="auto"
|
||||||
|
>
|
||||||
|
<FormItemRecursive
|
||||||
|
formType="input"
|
||||||
|
:current-list="formData.inputParams"
|
||||||
|
prop-path="inputParams"
|
||||||
|
:depth="0"
|
||||||
|
:is-first-level="true"
|
||||||
|
:endDepth="2"
|
||||||
|
:parent-path="[]"
|
||||||
|
@delete-item="(path) => deleteTopLevelItem(path, 'inputParams')"
|
||||||
|
/>
|
||||||
|
</el-form>
|
||||||
|
</el-collapse-item>
|
||||||
|
</el-collapse>
|
||||||
</template>
|
</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 () => {
|
||||||
let inputValid = true
|
const names = formData.resourceRoles.map((role) => role.displayName.trim())
|
||||||
if (dynamicFormRef.value) {
|
const invalidRole = formData.resourceRoles.some((role) => !role.displayName.trim())
|
||||||
await dynamicFormRef.value.validate((valid) => { if (!valid) inputValid = false })
|
const duplicateRole = new Set(names).size !== names.length
|
||||||
}
|
const invalidSlot = formData.resourceRoles.some((role) => role.deviceSlots.some((slot) => (
|
||||||
|
!slot.displayName.trim() || slot.deviceKind == null
|
||||||
|
)))
|
||||||
|
|
||||||
// 保存到 lf
|
if (invalidRole || duplicateRole || invalidSlot) {
|
||||||
lf.setProperties(props.data.id, {
|
ElMessage.warning(invalidRole ? '请填写机器人角色名称' : duplicateRole ? '机器人角色名称不能重复' : '请完整填写设备用途和类型')
|
||||||
...props.data.properties,
|
emit('save-error')
|
||||||
...formData
|
return
|
||||||
})
|
}
|
||||||
if (inputValid) {
|
|
||||||
emit('save-success')
|
let inputValid = true
|
||||||
} else {
|
if (dynamicFormRef.value) {
|
||||||
emit('save-error')
|
await dynamicFormRef.value.validate((valid) => { if (!valid) inputValid = false })
|
||||||
}
|
}
|
||||||
|
globalThis.lf.setProperties(props.data.id, {
|
||||||
|
...props.data.properties,
|
||||||
|
schemaVersion: 2,
|
||||||
|
resourceRoles: JSON.parse(JSON.stringify(formData.resourceRoles)),
|
||||||
|
inputParams: JSON.parse(JSON.stringify(formData.inputParams)),
|
||||||
|
})
|
||||||
|
emit(inputValid ? 'save-success' : 'save-error')
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({ validateAndSave })
|
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);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.el-row) {
|
.role-row {
|
||||||
align-items: end;
|
margin-bottom: 14px;
|
||||||
|
padding: 14px;
|
||||||
|
border: 1px solid var(--el-border-color-light);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
.el-form-item {
|
.role-row__header,
|
||||||
margin-right: 12px;
|
.device-row {
|
||||||
}
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.param-name {
|
.role-row__section {
|
||||||
width: 160px;
|
margin-top: 14px;
|
||||||
}
|
padding-top: 12px;
|
||||||
|
border-top: 1px solid var(--el-border-color-lighter);
|
||||||
|
}
|
||||||
|
|
||||||
.param-type {
|
.role-row__label {
|
||||||
width: 80px;
|
margin-bottom: 8px;
|
||||||
}
|
color: var(--el-text-color-regular);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
.param-value {
|
.device-row {
|
||||||
width: 160px;
|
grid-template-columns: minmax(0, 1fr) 132px auto;
|
||||||
}
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
.empty-tip {
|
||||||
|
margin-bottom: 6px;
|
||||||
|
color: var(--el-text-color-placeholder);
|
||||||
|
font-size: 12px;
|
||||||
.sub-properties {
|
|
||||||
margin-left: 10px;
|
|
||||||
|
|
||||||
.zw {
|
|
||||||
margin-left: 15px;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: -4px;
|
|
||||||
width: 10px;
|
|
||||||
border-left: 1px solid gray;
|
|
||||||
border-bottom: 1px solid gray;
|
|
||||||
border-bottom-left-radius: 4px;
|
|
||||||
background-color: transparent;
|
|
||||||
height: 24px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.gSon-properties {
|
|
||||||
margin-left: 10px;
|
|
||||||
|
|
||||||
.zw {
|
|
||||||
margin-left: 15px;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: -4px;
|
|
||||||
width: 10px;
|
|
||||||
border-left: 1px solid gray;
|
|
||||||
border-bottom: 1px solid gray;
|
|
||||||
border-bottom-left-radius: 4px;
|
|
||||||
background-color: transparent;
|
|
||||||
height: 24px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.deleteBtn {
|
|
||||||
margin-bottom: 22px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -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) {
|
||||||
result[0] = idMap.get(result[0]);
|
if (idMap.has(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>
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -123,12 +125,23 @@ const confirm = () => {
|
|||||||
ruleFormRef.value
|
ruleFormRef.value
|
||||||
.validate()
|
.validate()
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
const runParams = {}
|
const runParams = {}
|
||||||
formData.value.tableData.forEach(item => {
|
formData.value.tableData.forEach(item => {
|
||||||
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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user