CMVR-IOT-UI/src/views/flow/components/TestRun.vue
lixiaolong b2593bf857 fix(flow): 优化机器人设备绑定逻辑并改进状态提示
- 保留原始设备ID和选项作为默认值,避免数据丢失
- 当机器人无匹配在线设备时显示警告并保留原有设置
- 改进设备选择逻辑,支持设备种类过滤和自动填充
- 添加未触动手动设备节点统计功能
- 更新绑定面板状态显示,区分缺失设备和手动设备
- 优化错误处理机制,失败时恢复原始参数值
- 完善设备匹配成功和失败的消息提示内容
2026-08-05 10:46:11 +08:00

261 lines
9.7 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 bindingLoading = ref(false)
const emptyBindingSummary = (robotId = '') => ({ robotId, matched: 0, missing: 0, untouched: 0 })
const bindingSummary = ref(emptyBindingSummary())
const emits = defineEmits(['close', 'changeState'])
const handleClose = () => {
ruleFormRef.value?.resetFields()
emits('close')
}
watch(() => props.drawer,
(newVal) => {
if (newVal) {
const data = getStartNodeFormData()
tableData.value = data
bindingSummary.value = emptyBindingSummary()
loadRobotOptions()
}
}
)
const confirm = () => {
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 () => {
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
}
})
}
}
</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>