CMVR-BID/src/views/flow/components/TestRun.vue
lixiaolong 8e2afcbaa6 feat(flow): 添加单节点执行机器人选择记忆功能
- 引入 watch 监听器和相关变量用于缓存单节点机器人选择
- 实现按节点ID缓存机器人选择,新节点默认沿用最近一次选择
- 在单节点执行前后记录当前机器人选择到缓存中
- 修改NodeTitle组件传递节点ID用于按节点恢复机器人选择
- 添加试运行参数缓存功能,支持按流程缓存整套参数
- 实现参数层级缓存值恢复,兼容参数结构变化
- 优化试运行关闭和确认时的数据缓存处理逻辑
2026-08-07 11:28:01 +08:00

294 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<el-drawer
:model-value="props.drawer"
title="试运行"
:before-close="handleClose"
>
<el-form
ref="ruleFormRef"
style="max-width: 600px"
:model="{ tableData }"
:rules="rules"
label-width="auto"
>
<div class="binding-panel" :class="{ 'binding-panel--warning': bindingSummary.missing > 0 }">
<div class="binding-panel__title">
<span class="binding-panel__dot"></span>
机器人设备自动匹配
</div>
<div class="binding-panel__text" v-if="bindingLoading">正在读取机器人在线设备...</div>
<div class="binding-panel__text" v-else-if="bindingSummary.robotId">
已匹配 {{ bindingSummary.matched }} 个设备节点
<span v-if="bindingSummary.missing">,{{ bindingSummary.missing }} 个节点未匹配并保留原值</span>
<span v-if="bindingSummary.untouched">,{{ bindingSummary.untouched }} 个手动设备节点保持不变</span>
</div>
<div class="binding-panel__text" v-else>选择机器人后,将按节点类型自动填充该机器人上报的在线设备。</div>
</div>
<el-table
:data="tableData"
style="width: 100%; margin-bottom: 20px"
row-key="name"
default-expand-all
>
<el-table-column prop="name" label="入参名称">
<template #default="{row}">
<span>{{ row.name }}</span>
<span v-if="row.required" style="color: #f56c6c; margin-left: 8px;">*</span>
</template>
</el-table-column>
<el-table-column prop="type" label="入参类型">
<template #default="scope">
<el-tag>{{ scope.row.type }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="value" label="入参值">
<template #default="{ row, column, $index }">
<el-form-item v-if="row.name === 'robotId'" label="" :prop="`tableData${row.propPath}value`" :rules="row.required ? [{ required: true, message: '请选择执行机器人', trigger: 'change' }] : []">
<el-select v-model="row.value" filterable style="width: 100%" @change="handleRobotChange">
<el-option
v-for="item in robotIdOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item v-else-if="row.type !== 'object' && !row.type.includes('array')" label="" :prop="`tableData${row.propPath}value`" :rules="row.required ? [{ required: true, message: '请输入变量名', trigger: 'blur' }] : []">
<el-input v-if="row.type === 'string'" v-model="row.value" />
<el-input-number v-if="row.type === 'number'" :controls="false" v-model="row.value" />
<el-switch v-if="row.type === 'boolean'" v-model="row.value" />
</el-form-item>
<el-form-item v-else-if="row.type !== 'object' && row.type.includes('array')" label="" :prop="`tableData${row.propPath}value`" :rules="row.required ? [{ required: true, message: '请输入变量名', trigger: 'blur' }] : []">
<el-input v-model="row.value" placeholder="请输入数组值,格式如:[1,2,3]" />
</el-form-item>
</template>
</el-table-column>
</el-table>
</el-form>
<template #footer>
<el-button @click="handleClose">关闭</el-button>
<el-button type="primary" :loading="bindingLoading" :disabled="bindingLoading" @click="confirm">确定</el-button>
</template>
</el-drawer>
</template>
<script setup>
import { ref, watch } from 'vue'
import { getStartNodeFormData, formatTableData } from '@/utils/flow'
// import { Plus, Minus } from '@element-plus/icons-vue'
import { flowExecuteTrial } from '@/api/flow/flow'
import { emitter } from '@/utils/eventBus';
import { ElMessage } from 'element-plus';
import { getRobotList, getRobotDevicesByRobotId } from '@/api/inspection/robot'
import { getDeviceKindForAction, toDeviceOptions } from '@/utils/robotDevice'
const props = defineProps({
drawer: Boolean,
flowId: String
})
const ruleFormRef = ref()
const rules = ref({});
const tableData = ref([])
// 按流程缓存整套试运行参数,关闭或执行后再次打开时继续使用。
const trialRunCache = new Map()
const bindingLoading = ref(false)
const emptyBindingSummary = (robotId = '') => ({ robotId, matched: 0, missing: 0, untouched: 0 })
const bindingSummary = ref(emptyBindingSummary())
const emits = defineEmits(['close', 'changeState'])
const cloneData = (data) => JSON.parse(JSON.stringify(data || []))
// 根据参数层级恢复缓存值,同时兼容流程参数结构发生增删的情况。
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
})
}
const cacheKey = () => props.flowId || '__new_flow__'
// 深拷贝快照,避免后续表单编辑直接修改缓存内容。
const rememberTrialRunParams = () => {
if (!tableData.value.length) return
trialRunCache.set(cacheKey(), cloneData(tableData.value))
}
const handleClose = () => {
rememberTrialRunParams()
ruleFormRef.value?.clearValidate()
emits('close')
}
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()
}
}
)
const confirm = () => {
rememberTrialRunParams()
lf.fitView()
ruleFormRef.value.validate().then(async () => {
const formData = formatTableData(tableData.value)
const { nodes } = lf.getGraphData()
nodes.forEach(item => {
emitter.emit('contentChange', { id: item.id})
})
const flowData = JSON.stringify(lf.getGraphData())
const { robotId, ...runParams } = formData
const res = await flowExecuteTrial({
flowData,
itemId: props.flowId,
robotId,
runParams
})
if (res.code === 200) {
emits('changeState', {
type: 'testRunning',
instId: res.msg
})
} else {
ElMessage.error(res.msg)
}
})
.catch(() => {})
}
const robotIdOptions = ref([])
const handleRobotChange = async (robotId) => {
bindingSummary.value = emptyBindingSummary(robotId || '')
if (!robotId) return
bindingLoading.value = true
try {
const response = await getRobotDevicesByRobotId(robotId)
const devices = Array.isArray(response.data) ? response.data : []
let matched = 0
let missing = 0
let untouched = 0
const missingNodes = []
const { nodes } = lf.getGraphData()
nodes.forEach((node) => {
if (node.properties?.nodeType !== 'EDGE') return
const nodeParams = JSON.parse(JSON.stringify(node.properties.nodeParams || []))
const robotParam = nodeParams.find(param => param.name === 'robotId')
if (robotParam) robotParam.input = robotId
const deviceParam = nodeParams.find(param => param.name === 'deviceId')
if (!deviceParam) {
if (robotParam) lf.setProperties(node.id, { ...node.properties, nodeParams })
return
}
const kind = getDeviceKindForAction(node.properties.action)
if (!kind) {
untouched += 1
if (robotParam) lf.setProperties(node.id, { ...node.properties, nodeParams })
return
}
const candidates = devices.filter(device => Number(device.deviceKind) === kind)
if (!candidates.length) {
missing += 1
missingNodes.push(node.properties.name || node.id)
if (robotParam) lf.setProperties(node.id, { ...node.properties, nodeParams })
return
}
const currentCandidate = candidates.find(device => device.deviceId === deviceParam.input)
deviceParam.input = (currentCandidate || candidates[0]).deviceId
deviceParam.componentType = 'select'
deviceParam.selectOptions = toDeviceOptions(candidates)
deviceParam.deviceLocked = candidates.length === 1
lf.setProperties(node.id, { ...node.properties, nodeParams })
matched += 1
})
bindingSummary.value = { robotId, matched, missing, untouched }
if (missingNodes.length) {
ElMessage.warning(`以下节点没有匹配的在线设备,已保留原值:${missingNodes.join('、')}`)
} else if (matched > 0) {
ElMessage.success(`已自动匹配 ${matched} 个设备节点`)
}
} catch (error) {
bindingSummary.value = emptyBindingSummary(robotId)
console.error('自动匹配机器人设备失败:', error)
ElMessage.error('读取机器人设备失败')
} finally {
bindingLoading.value = false
}
}
/** 获取在线机器人列表 */
const loadRobotOptions = async () => {
// 试运行不需要要有连接状态为连接的,因为可以选择未连接的机器人进行试运行来保存流程数据
// , connectStatus: '1'
const res = await getRobotList({ pageNum: 1, pageSize: 1000 })
if (res.code === 200) {
robotIdOptions.value = res.rows?.map((item) => {
return {
label: `${item.robotName || item.robotId} (${item.robotId})`,
value: item.robotId
}
})
}
}
</script>
<style lang="scss" scoped>
.binding-panel {
margin-bottom: 16px;
padding: 12px 14px;
border: 1px solid #d7e5f7;
border-radius: 6px;
background: #f5f9ff;
}
.binding-panel--warning {
border-color: #f3d19e;
background: #fdf6ec;
}
.binding-panel__title {
display: flex;
align-items: center;
gap: 8px;
color: #24364b;
font-weight: 600;
}
.binding-panel__dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #409eff;
}
.binding-panel__text {
margin-top: 6px;
color: #64748b;
font-size: 13px;
line-height: 20px;
}
</style>