320 lines
11 KiB
Vue
320 lines
11 KiB
Vue
<!-- 流程试运行:分配机器人角色并收集业务运行参数。 -->
|
||
<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>
|