2026-08-11 16:51:58 +08:00
|
|
|
<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"
|
2026-08-12 09:31:45 +08:00
|
|
|
clearable
|
2026-08-11 16:51:58 +08:00
|
|
|
filterable
|
|
|
|
|
:loading="robotLoading"
|
|
|
|
|
placeholder="请选择机器人"
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
@change="(robotId) => changeRobot(role, robotId)"
|
|
|
|
|
>
|
2026-08-12 09:31:45 +08:00
|
|
|
<el-option v-for="item in robotOptionsOf(role)" :key="item.value" :label="item.label" :value="item.value" />
|
2026-08-11 16:51:58 +08:00
|
|
|
</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>
|
2026-08-12 09:31:45 +08:00
|
|
|
<el-alert v-if="loadError" :title="loadError" type="error" :closable="false" show-icon />
|
2026-08-11 16:51:58 +08:00
|
|
|
<el-empty v-if="!roles.length" :image-size="52" description="当前任务不需要机器人" />
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup>
|
2026-08-12 09:31:45 +08:00
|
|
|
import { computed, reactive, ref, watch } from 'vue'
|
2026-08-11 16:51:58 +08:00
|
|
|
import { getRobotList, getRobotDevicesByRobotId } from '@/api/inspection/robot'
|
|
|
|
|
import { toDeviceOptions } from '@/utils/robotDevice'
|
|
|
|
|
|
|
|
|
|
const props = defineProps({
|
|
|
|
|
roles: { type: Array, default: () => [] },
|
|
|
|
|
modelValue: { type: Array, default: () => [] },
|
2026-08-12 09:31:45 +08:00
|
|
|
robotQuery: { type: Object, default: () => ({}) },
|
|
|
|
|
requiredMapId: { type: [String, Number], default: '' },
|
2026-08-11 16:51:58 +08:00
|
|
|
})
|
|
|
|
|
const emit = defineEmits(['update:modelValue'])
|
|
|
|
|
|
|
|
|
|
const assignments = ref([])
|
|
|
|
|
const robotOptions = ref([])
|
|
|
|
|
const devicesByRole = reactive({})
|
|
|
|
|
const robotLoading = ref(false)
|
|
|
|
|
const loadingRoleKey = ref('')
|
2026-08-12 09:31:45 +08:00
|
|
|
const robotLoadError = ref('')
|
|
|
|
|
const deviceLoadErrors = reactive({})
|
|
|
|
|
const loadError = computed(() => robotLoadError.value || Object.values(deviceLoadErrors).find(Boolean) || '')
|
2026-08-11 16:51:58 +08:00
|
|
|
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
|
2026-08-12 09:31:45 +08:00
|
|
|
robotLoadError.value = ''
|
2026-08-11 16:51:58 +08:00
|
|
|
try {
|
2026-08-12 09:31:45 +08:00
|
|
|
const response = await getRobotList({
|
|
|
|
|
pageNum: 1,
|
|
|
|
|
pageSize: 1000,
|
|
|
|
|
connectStatus: '1',
|
|
|
|
|
...props.robotQuery,
|
|
|
|
|
})
|
2026-08-11 16:51:58 +08:00
|
|
|
robotOptions.value = (response.rows || []).map((robot) => ({
|
|
|
|
|
label: `${robot.robotName || robot.robotId} (${robot.robotId})`,
|
|
|
|
|
value: robot.robotId,
|
2026-08-12 09:31:45 +08:00
|
|
|
currentMapId: robot.currentMapId,
|
2026-08-11 16:51:58 +08:00
|
|
|
}))
|
2026-08-12 09:31:45 +08:00
|
|
|
} catch (error) {
|
|
|
|
|
robotOptions.value = []
|
|
|
|
|
robotLoadError.value = '机器人列表加载失败,请稍后重试'
|
2026-08-11 16:51:58 +08:00
|
|
|
} finally {
|
|
|
|
|
robotLoading.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-12 09:31:45 +08:00
|
|
|
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)
|
|
|
|
|
)))
|
|
|
|
|
}
|
2026-08-11 16:51:58 +08:00
|
|
|
|
|
|
|
|
const loadDevices = async (role, preserve = true) => {
|
|
|
|
|
const member = memberOf(role)
|
2026-08-12 09:31:45 +08:00
|
|
|
if (!member.robotId) {
|
|
|
|
|
devicesByRole[role.roleKey] = []
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-08-11 16:51:58 +08:00
|
|
|
loadingRoleKey.value = role.roleKey
|
2026-08-12 09:31:45 +08:00
|
|
|
delete deviceLoadErrors[role.roleKey]
|
2026-08-11 16:51:58 +08:00
|
|
|
try {
|
|
|
|
|
const response = await getRobotDevicesByRobotId(member.robotId)
|
|
|
|
|
devicesByRole[role.roleKey] = Array.isArray(response.data) ? response.data : []
|
2026-08-12 09:31:45 +08:00
|
|
|
;(role.deviceSlots || []).forEach((slot) => {
|
2026-08-11 16:51:58 +08:00
|
|
|
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()
|
2026-08-12 09:31:45 +08:00
|
|
|
} catch (error) {
|
|
|
|
|
devicesByRole[role.roleKey] = []
|
|
|
|
|
deviceLoadErrors[role.roleKey] = `“${role.displayName}”的设备列表加载失败,请重新选择机器人`
|
2026-08-11 16:51:58 +08:00
|
|
|
} finally {
|
|
|
|
|
loadingRoleKey.value = ''
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const changeRobot = async (role, robotId) => {
|
|
|
|
|
const member = memberOf(role)
|
|
|
|
|
member.robotId = robotId
|
2026-08-12 09:31:45 +08:00
|
|
|
member.devices = Object.fromEntries((role.deviceSlots || []).map((slot) => [slot.slotKey, '']))
|
2026-08-11 16:51:58 +08:00
|
|
|
await loadDevices(role, false)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const initialize = async () => {
|
2026-08-12 09:31:45 +08:00
|
|
|
Object.keys(deviceLoadErrors).forEach((roleKey) => delete deviceLoadErrors[roleKey])
|
2026-08-11 16:51:58 +08:00
|
|
|
const existing = clone(props.modelValue)
|
2026-08-12 09:31:45 +08:00
|
|
|
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 } : {}),
|
2026-08-12 14:13:16 +08:00
|
|
|
...(role.itemOccurrence != null ? { itemOccurrence: role.itemOccurrence } : {}),
|
2026-08-12 09:31:45 +08:00
|
|
|
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 || '',
|
|
|
|
|
])),
|
|
|
|
|
}],
|
|
|
|
|
}
|
|
|
|
|
})
|
2026-08-11 16:51:58 +08:00
|
|
|
emitValue()
|
|
|
|
|
await loadRobots()
|
|
|
|
|
await Promise.all(props.roles.filter((role) => memberOf(role).robotId).map((role) => loadDevices(role)))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const validate = () => {
|
2026-08-12 09:31:45 +08:00
|
|
|
const physicalDeviceUses = new Map()
|
2026-08-11 16:51:58 +08:00
|
|
|
for (const role of props.roles) {
|
|
|
|
|
const member = memberOf(role)
|
|
|
|
|
if (role.required && !member.robotId) return `请为“${role.displayName}”选择机器人`
|
2026-08-12 09:31:45 +08:00
|
|
|
if (!member.robotId) continue
|
2026-08-11 16:51:58 +08:00
|
|
|
for (const slot of role.deviceSlots || []) {
|
|
|
|
|
if (slot.required && !member.devices[slot.slotKey]) return `请为“${role.displayName}”选择“${slot.displayName}”`
|
2026-08-12 09:31:45 +08:00
|
|
|
const deviceId = member.devices[slot.slotKey]
|
|
|
|
|
if (!deviceId) continue
|
2026-08-12 14:13:16 +08:00
|
|
|
// 不同检测项按顺序独立执行,可以复用同一台机器人及其设备。
|
|
|
|
|
// 仅同一检测项内部的多个角色/用途需要遵守设备复用规则。
|
|
|
|
|
const itemScope = role.itemId == null
|
|
|
|
|
? 'task'
|
|
|
|
|
: `${role.itemId}:${role.itemOccurrence || 1}`
|
|
|
|
|
const physicalKey = `${itemScope}:${member.robotId}:${deviceId}`
|
2026-08-12 09:31:45 +08:00
|
|
|
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,
|
|
|
|
|
})
|
2026-08-11 16:51:58 +08:00
|
|
|
}
|
|
|
|
|
}
|
2026-08-12 09:31:45 +08:00
|
|
|
if (loadError.value) return loadError.value
|
2026-08-11 16:51:58 +08:00
|
|
|
return ''
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-12 09:31:45 +08:00
|
|
|
watch([() => props.roles, () => props.robotQuery, () => props.requiredMapId], initialize, { immediate: true, deep: true })
|
2026-08-11 16:51:58 +08:00
|
|
|
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>
|