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

231 lines
8.7 KiB
Vue
Raw Normal View History

<template>
<div class="runtime-assignments">
<div v-for="role in roles" :key="role.roleKey" class="runtime-role">
<div class="runtime-role__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="role.required">
<el-select
:model-value="memberOf(role).robotId"
clearable
filterable
:loading="robotLoading"
placeholder="请选择机器人"
style="width: 100%"
@change="(robotId) => changeRobot(role, robotId)"
>
<el-option v-for="item in robotOptionsOf(role)" :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="memberOf(role).devices[slot.slotKey]"
filterable
:loading="loadingRoleKey === role.roleKey"
:disabled="!memberOf(role).robotId"
:placeholder="memberOf(role).robotId ? '请选择设备' : '请先选择机器人'"
style="width: 100%"
@change="emitValue"
>
<el-option v-for="item in optionsOf(role, slot)" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<div v-if="memberOf(role).robotId && !optionsOf(role, slot).length" class="runtime-role__error">
该机器人没有匹配的在线设备
</div>
</el-form-item>
</el-form>
</div>
<el-alert v-if="loadError" :title="loadError" type="error" :closable="false" show-icon />
<el-empty v-if="!roles.length" :image-size="52" description="当前任务不需要机器人" />
</div>
</template>
<script setup>
import { computed, reactive, ref, watch } from 'vue'
import { getRobotList, getRobotDevicesByRobotId } from '@/api/inspection/robot'
import { toDeviceOptions } from '@/utils/robotDevice'
const props = defineProps({
roles: { type: Array, default: () => [] },
modelValue: { type: Array, default: () => [] },
robotQuery: { type: Object, default: () => ({}) },
requiredMapId: { type: [String, Number], default: '' },
})
const emit = defineEmits(['update:modelValue'])
const assignments = ref([])
const robotOptions = ref([])
const devicesByRole = reactive({})
const robotLoading = ref(false)
const loadingRoleKey = ref('')
const robotLoadError = ref('')
const deviceLoadErrors = reactive({})
const loadError = computed(() => robotLoadError.value || Object.values(deviceLoadErrors).find(Boolean) || '')
const clone = (value) => JSON.parse(JSON.stringify(value || []))
const assignmentOf = (role) => assignments.value.find((item) => item.roleKey === role.roleKey)
const memberOf = (role) => assignmentOf(role).members[0]
const emitValue = () => emit('update:modelValue', clone(assignments.value))
const loadRobots = async () => {
robotLoading.value = true
robotLoadError.value = ''
try {
const response = await getRobotList({
pageNum: 1,
pageSize: 1000,
connectStatus: '1',
...props.robotQuery,
})
robotOptions.value = (response.rows || []).map((robot) => ({
label: `${robot.robotName || robot.robotId} (${robot.robotId})`,
value: robot.robotId,
currentMapId: robot.currentMapId,
}))
} catch (error) {
robotOptions.value = []
robotLoadError.value = '机器人列表加载失败,请稍后重试'
} finally {
robotLoading.value = false
}
}
const robotOptionsOf = (role) => {
const requiresTaskMap = (role.deviceSlots || []).some((slot) => Number(slot.deviceKind) === 1)
if (!props.requiredMapId || !requiresTaskMap) return robotOptions.value
return robotOptions.value.filter((robot) => String(robot.currentMapId || '') === String(props.requiredMapId))
}
const optionsOf = (role, slot) => {
const member = memberOf(role)
const unavailableDeviceIds = new Set((role.deviceSlots || [])
.filter((otherSlot) => (
otherSlot.slotKey !== slot.slotKey
&& (!slot.allowReuse || !otherSlot.allowReuse)
))
.map((otherSlot) => member.devices[otherSlot.slotKey])
.filter(Boolean))
return toDeviceOptions((devicesByRole[role.roleKey] || []).filter((device) => (
(slot.deviceKind == null || Number(device.deviceKind) === Number(slot.deviceKind))
&& (!unavailableDeviceIds.has(device.deviceId) || member.devices[slot.slotKey] === device.deviceId)
)))
}
const loadDevices = async (role, preserve = true) => {
const member = memberOf(role)
if (!member.robotId) {
devicesByRole[role.roleKey] = []
return
}
loadingRoleKey.value = role.roleKey
delete deviceLoadErrors[role.roleKey]
try {
const response = await getRobotDevicesByRobotId(member.robotId)
devicesByRole[role.roleKey] = Array.isArray(response.data) ? response.data : []
;(role.deviceSlots || []).forEach((slot) => {
const options = optionsOf(role, slot)
if (!preserve || !options.some((option) => option.value === member.devices[slot.slotKey])) {
member.devices[slot.slotKey] = options.length === 1 ? options[0].value : ''
}
})
emitValue()
} catch (error) {
devicesByRole[role.roleKey] = []
deviceLoadErrors[role.roleKey] = `“${role.displayName}”的设备列表加载失败,请重新选择机器人`
} finally {
loadingRoleKey.value = ''
}
}
const changeRobot = async (role, robotId) => {
const member = memberOf(role)
member.robotId = robotId
member.devices = Object.fromEntries((role.deviceSlots || []).map((slot) => [slot.slotKey, '']))
await loadDevices(role, false)
}
const initialize = async () => {
Object.keys(deviceLoadErrors).forEach((roleKey) => delete deviceLoadErrors[roleKey])
const existing = clone(props.modelValue)
assignments.value = props.roles.map((role) => {
const existingAssignment = existing.find((item) => item.roleKey === role.roleKey)
const existingMember = existingAssignment?.members?.[0]
return {
roleKey: role.roleKey,
2026-08-12 12:21:06 +08:00
...(role.sourceRoleKey ? { sourceRoleKey: role.sourceRoleKey } : {}),
...(role.itemId != null ? { itemId: role.itemId } : {}),
...(role.itemOccurrence != null ? { itemOccurrence: role.itemOccurrence } : {}),
members: [{
...existingMember,
memberKey: `${role.roleKey}_1`,
robotId: existingMember?.robotId || role.legacyRobotId || '',
devices: Object.fromEntries((role.deviceSlots || []).map((slot) => [
slot.slotKey,
existingMember?.devices?.[slot.slotKey] || slot.legacyDeviceId || '',
])),
}],
}
})
emitValue()
await loadRobots()
await Promise.all(props.roles.filter((role) => memberOf(role).robotId).map((role) => loadDevices(role)))
}
const validate = () => {
const physicalDeviceUses = new Map()
for (const role of props.roles) {
const member = memberOf(role)
if (role.required && !member.robotId) return `请为“${role.displayName}”选择机器人`
if (!member.robotId) continue
for (const slot of role.deviceSlots || []) {
if (slot.required && !member.devices[slot.slotKey]) return `请为“${role.displayName}”选择“${slot.displayName}”`
const deviceId = member.devices[slot.slotKey]
if (!deviceId) continue
// 不同检测项按顺序独立执行,可以复用同一台机器人及其设备。
// 仅同一检测项内部的多个角色/用途需要遵守设备复用规则。
const itemScope = role.itemId == null
? 'task'
: `${role.itemId}:${role.itemOccurrence || 1}`
const physicalKey = `${itemScope}:${member.robotId}:${deviceId}`
const existingUse = physicalDeviceUses.get(physicalKey)
if (existingUse && (!slot.allowReuse || !existingUse.allowReuse)) {
return `“${existingUse.roleName} / ${existingUse.slotName}”和“${role.displayName} / ${slot.displayName}”不能使用同一个设备`
}
physicalDeviceUses.set(physicalKey, {
allowReuse: slot.allowReuse,
roleName: role.displayName,
slotName: slot.displayName,
})
}
}
if (loadError.value) return loadError.value
return ''
}
watch([() => props.roles, () => props.robotQuery, () => props.requiredMapId], initialize, { immediate: true, deep: true })
defineExpose({ validate })
</script>
<style scoped lang="scss">
.runtime-role {
padding: 12px 0 4px;
border-bottom: 1px solid var(--el-border-color-lighter);
}
.runtime-role__title {
display: flex;
gap: 8px;
align-items: center;
margin-bottom: 10px;
font-weight: 650;
}
.runtime-role__error {
margin-top: 5px;
color: var(--el-color-danger);
font-size: 12px;
}
</style>