CMVR-IOT-UI/src/views/flow/components/TestRun.vue

320 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="试运行" size="min(620px, 96vw)" :before-close="handleClose">
<div class="section-heading">本次执行安排</div>
<div class="assignment-tip">流程只保存角色分工,以下机器人和设备仅用于本次运行。</div>
<div v-for="role in roles" :key="role.roleKey" class="assignment-row">
<div class="assignment-row__title">
<span>{{ role.displayName }}</span>
<el-tag v-if="role.required" size="small" type="info">必需</el-tag>
</div>
<el-form label-position="top">
<el-form-item label="执行机器人" required>
<el-select
:model-value="getMember(role).robotId"
filterable
: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>
<template #footer>
<el-button @click="handleClose">关闭</el-button>
<el-button type="primary" :loading="submitting" @click="confirm">开始运行</el-button>
</template>
</el-drawer>
</template>
<script setup>
import { reactive, ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { flowExecuteTrial } from '@/api/flow/flow'
import { getRobotList, getRobotDevicesByRobotId } from '@/api/inspection/robot'
import { emitter } from '@/utils/eventBus'
import { formatTableData, getStartNodeFormData } from '@/utils/flow'
import { toDeviceOptions } from '@/utils/robotDevice'
import {
getResourceRoles,
normalizeFlowResourceSchema,
sanitizeFlowResourceDefinition,
validateFlowResources,
} from '@/utils/flowResources'
const props = defineProps({ drawer: Boolean, flowId: String })
const emits = defineEmits(['close', 'changeState'])
const ruleFormRef = ref()
const tableData = ref([])
const roles = ref([])
const runtimeAssignments = ref([])
const robotOptions = ref([])
const devicesByRole = reactive({})
const robotLoading = ref(false)
const loadingRoleKey = ref('')
const submitting = ref(false)
const trialRunCache = new Map()
const clone = (value) => JSON.parse(JSON.stringify(value || []))
const cacheKey = () => props.flowId || '__new_flow__'
const mergeCachedValues = (source, cached) => {
const cachedByName = new Map((cached || []).map((item) => [item.name, item]))
return (source || []).map((item, index) => {
const cachedItem = cached?.[index]?.name === item.name ? cached[index] : 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 getAssignment = (role) => runtimeAssignments.value.find((item) => item.roleKey === role.roleKey)
const getMember = (role) => getAssignment(role).members[0]
const createAssignments = (cached = []) => roles.value.map((role) => {
const old = cached.find((item) => item.roleKey === role.roleKey)
return old || {
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),
})
}
const loadRobotOptions = async () => {
robotLoading.value = true
try {
const response = await getRobotList({ pageNum: 1, pageSize: 1000, connectStatus: 1 })
robotOptions.value = (response.rows || []).map((robot) => ({
label: `${robot.robotName || robot.robotId} (${robot.robotId})`,
value: robot.robotId,
}))
} finally {
robotLoading.value = false
}
}
const getSlotOptions = (role, slot) => toDeviceOptions((devicesByRole[role.roleKey] || []).filter((device) => (
slot.deviceKind == null || Number(device.deviceKind) === Number(slot.deviceKind)
)))
const loadRoleDevices = async (role, preserveValues = true) => {
const member = getMember(role)
if (!member.robotId) {
devicesByRole[role.roleKey] = []
return
}
loadingRoleKey.value = role.roleKey
try {
const response = await getRobotDevicesByRobotId(member.robotId)
devicesByRole[role.roleKey] = Array.isArray(response.data) ? response.data : []
role.deviceSlots.forEach((slot) => {
const options = getSlotOptions(role, slot)
const currentValid = options.some((option) => option.value === member.devices[slot.slotKey])
if (!preserveValues || !currentValid) member.devices[slot.slotKey] = options.length === 1 ? options[0].value : ''
})
} catch (error) {
devicesByRole[role.roleKey] = []
ElMessage.error(`读取“${role.displayName}”的设备失败`)
} finally {
loadingRoleKey.value = ''
}
}
const handleRobotChange = async (role, robotId) => {
const member = getMember(role)
member.robotId = robotId
member.devices = Object.fromEntries(role.deviceSlots.map((slot) => [slot.slotKey, '']))
await loadRoleDevices(role, false)
}
const initialize = async () => {
const graph = normalizeFlowResourceSchema(globalThis.lf.getGraphData())
roles.value = getResourceRoles(graph)
const cached = trialRunCache.get(cacheKey())
tableData.value = mergeCachedValues(getStartNodeFormData(), cached?.tableData)
runtimeAssignments.value = createAssignments(cached?.runtimeAssignments)
await loadRobotOptions()
await Promise.all(roles.value.filter((role) => getMember(role).robotId).map((role) => loadRoleDevices(role)))
}
watch(() => props.drawer, (open) => { if (open) initialize() }, { immediate: true })
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>
<style scoped lang="scss">
.section-heading {
margin: 6px 0 10px;
color: var(--el-text-color-primary);
font-size: 15px;
font-weight: 650;
}
.section-heading--params {
margin-top: 24px;
}
.assignment-tip {
margin-bottom: 14px;
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;
line-height: 20px;
}
.assignment-row {
padding: 14px 0 4px;
border-bottom: 1px solid var(--el-border-color-lighter);
}
.assignment-row__title {
display: flex;
gap: 8px;
align-items: center;
margin-bottom: 12px;
font-weight: 650;
}
.assignment-error,
.required-mark {
color: var(--el-color-danger);
}
.assignment-error {
margin-top: 5px;
font-size: 12px;
}
.required-mark {
margin-left: 6px;
}
</style>