CMVR-IOT-UI/src/views/flow/components/TestRun.vue
lixiaolong 45b190ff02 feat(flow): 优化机器人设备绑定功能
- 将terminalId替换为robotId以统一机器人标识
- 实现机器人设备动态加载和选择功能
- 添加设备类型过滤和状态显示
- 重构设备注册页面为机器人设备管理界面
- 优化API调用参数传递方式
- 添加设备自动匹配和锁定机制
2026-08-05 09:27:55 +08:00

251 lines
9.1 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>
</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 bindingSummary = ref({ robotId: '', matched: 0, missing: 0 })
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 = { robotId: '', matched: 0, missing: 0 }
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 = { robotId: robotId || '', matched: 0, missing: 0 }
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
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)
const candidates = kind
? devices.filter(device => Number(device.deviceKind) === kind)
: devices
deviceParam.input = candidates[0]?.deviceId || ''
deviceParam.componentType = 'select'
deviceParam.selectOptions = toDeviceOptions(candidates)
deviceParam.deviceLocked = candidates.length === 1
lf.setProperties(node.id, { ...node.properties, nodeParams })
if (candidates.length > 0) matched += 1
else {
missing += 1
missingNodes.push(node.properties.name || node.id)
}
})
bindingSummary.value = { robotId, matched, missing }
if (missingNodes.length) {
ElMessage.warning(`以下节点没有匹配的在线设备:${missingNodes.join('、')}`)
} else if (matched > 0) {
ElMessage.success(`已自动匹配 ${matched} 个设备节点`)
}
} catch (error) {
bindingSummary.value = { robotId, matched: 0, missing: 0 }
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>