diff --git a/src/utils/robotDevice.js b/src/utils/robotDevice.js index 96c619b..6890543 100644 --- a/src/utils/robotDevice.js +++ b/src/utils/robotDevice.js @@ -21,6 +21,8 @@ export const DEVICE_KIND_BY_ACTION = Object.freeze({ AGV_MOVE_TO_STATION: DEVICE_KIND.AGV, ARM_MOVE_TO_POINT: DEVICE_KIND.ARM, ARM_MOVE_TO_J: DEVICE_KIND.ARM, + ARM_ENABLE: DEVICE_KIND.ARM, + ARM_DISABLE: DEVICE_KIND.ARM, TOUCH: DEVICE_KIND.ARM, BIO_HEAD_SPEAK_START: DEVICE_KIND.BIO_HEAD, BIO_HEAD_SPEAK_STOP: DEVICE_KIND.BIO_HEAD, diff --git a/src/views/flow/components/TaskExecutionLog.vue b/src/views/flow/components/TaskExecutionLog.vue index 0424098..f4bab02 100644 --- a/src/views/flow/components/TaskExecutionLog.vue +++ b/src/views/flow/components/TaskExecutionLog.vue @@ -143,7 +143,7 @@ const analysisOf = (log) => { ;[ 'analysisStatus', 'analysisType', 'profileCode', 'analysisModel', 'analysisTimingMs', 'mediaUrl', 'audioUrl', 'videoUrl', 'imageUrl', 'label', 'labelName', 'matched', - 'confidence', 'passed', 'summary', 'conclusion', 'events', + 'confidence', 'passed', 'summary', 'conclusion', 'events', '_executionContext', ].forEach((key) => delete detailSource[key]) const hasDetails = Object.keys(detailSource).length > 0 const result = { diff --git a/src/views/flow/config.js b/src/views/flow/config.js index 0c1c03d..01d2e98 100644 --- a/src/views/flow/config.js +++ b/src/views/flow/config.js @@ -389,6 +389,28 @@ function handler(params) { ], outputType: 'json' }, + { + icon: videoSvg, + name: "开启机械臂使能", + type: "serviceNode", + desc: "开启机械臂使能,后续运动节点不会自动使能", + action: 'ARM_ENABLE', + nodeType: 'EDGE', + nodeParams: [], + outputParams: [], + outputType: 'json' + }, + { + icon: videoSvg, + name: "关闭机械臂使能", + type: "serviceNode", + desc: "关闭机械臂使能", + action: 'ARM_DISABLE', + nodeType: 'EDGE', + nodeParams: [], + outputParams: [], + outputType: 'json' + }, { icon: cameraSvg, name: "开始监听报警事件", diff --git a/src/views/inspection/runTask/index.vue b/src/views/inspection/runTask/index.vue index 38ab438..1c5d842 100644 --- a/src/views/inspection/runTask/index.vue +++ b/src/views/inspection/runTask/index.vue @@ -506,7 +506,9 @@ const subscribeDebounced = debounce(async (sub) => { const callback = (data) => { if (data !== '500') { - if (data.status !== 0) { + const status = Number(data.status) + const terminal = [0, 2, 4].includes(status) + if (!terminal) { const steps = pointList_runningTask.value[data.taskId] || [] let activeStep = Number.isInteger(data.itemOrder) && data.itemOrder > 0 ? data.itemOrder - 1 diff --git a/src/views/intelligenceTest/runningTask/index.vue b/src/views/intelligenceTest/runningTask/index.vue index 1267fb4..87bad74 100644 --- a/src/views/intelligenceTest/runningTask/index.vue +++ b/src/views/intelligenceTest/runningTask/index.vue @@ -67,7 +67,8 @@
执行日志
- +
@@ -156,7 +157,8 @@ - + @@ -550,7 +552,9 @@ const subscribeDebounced = debounce(async (sub) => { const callback = (data) => { if (data !== '500') { - if (data.status !== 0) { + const status = Number(data.status) + const terminal = [0, 2, 4].includes(status) + if (!terminal) { const steps = pointList_runningTask.value[data.taskId] || [] let activeStep = Number.isInteger(data.itemOrder) && data.itemOrder > 0 ? data.itemOrder - 1 @@ -595,6 +599,7 @@ const logDialogData = ref([]) // 历史表格行里的 taskId 是任务定义 ID;日志组件使用它查询本次任务包含的全部检测项。 const logDialogTaskId = ref('') +const logDialogTaskStatus = ref(null) const logDialogLoading = ref(false) let logDialogRequestId = 0 const handleOpenLog = async (row) => { @@ -603,6 +608,7 @@ const handleOpenLog = async (row) => { // 在请求实例日志前先更新 taskId,让检测项请求与日志请求并行执行,减少弹窗等待时间。 logDialogTaskId.value = row.taskId + logDialogTaskStatus.value = row.status logDialogData.value = [] logDialogLoading.value = true try { diff --git a/src/views/intelligenceTest/runningTask/task-log.vue b/src/views/intelligenceTest/runningTask/task-log.vue index 8d19199..eff06ea 100644 --- a/src/views/intelligenceTest/runningTask/task-log.vue +++ b/src/views/intelligenceTest/runningTask/task-log.vue @@ -21,10 +21,16 @@ {{ String(index + 1).padStart(2, '0') }} - {{ group.name }} - - {{ group.status.label }} - + + {{ group.name }} + {{ group.repeatLabel }} + + + {{ group.status.label }} + + {{ group.analysisStatus.label }} + + @@ -62,6 +68,7 @@ import TaskExecutionLog from '@/views/flow/components/TaskExecutionLog.vue' */ const props = defineProps({ taskId: { type: [String, Number], default: '' }, + taskStatus: { type: [String, Number], default: null }, list: { type: Array, default: () => [] }, items: { type: Array, default: () => [] }, height: { type: Number, default: 150 }, @@ -120,6 +127,7 @@ const itemKeysOf = (item) => uniqueValues([ const logKeysOf = (log) => { const actualResult = parseObject(log?.actualResult) const extraInfo = parseObject(log?.extraInfo) + const executionContext = parseObject(actualResult?._executionContext) return uniqueValues([ log?.itemId, log?.detectItemId, @@ -130,11 +138,21 @@ const logKeysOf = (log) => { log?.item?.detectItemId, actualResult.itemId, actualResult.detectItemId, + executionContext.itemId, extraInfo.itemId, extraInfo.detectItemId, ]) } +const logExecutionContextOf = (log) => { + const actualResult = parseObject(log?.actualResult) + const context = parseObject(actualResult?._executionContext) + return { + itemOrder: log?.itemOrder ?? context.itemOrder, + itemOccurrence: log?.itemOccurrence ?? context.itemOccurrence, + } +} + // 检测项名称优先使用任务用例名称,其他字段作为不同后端版本的兼容回退。 const nameOf = (item) => item?.caseName || item?.detectName || item?.itemName || item?.name || '未命名检测项' @@ -158,12 +176,19 @@ const nodeNameOf = (log) => { // 状态统一转为小写字符串,使数字枚举和英文状态值可以使用同一套判断逻辑。 const normalizedStatus = (value) => String(value ?? '').trim().toLowerCase() +const isItemCompletionLog = (log) => { + const actualResult = parseObject(log?.actualResult) + const context = parseObject(actualResult?._executionContext) + return String(context.eventType || '').toUpperCase() === 'NODE_COMPLETED' + && String(context.nodeType || '').toUpperCase() === 'END' +} + /** * 根据一个检测项下的全部日志推导检测项状态。 * 优先级为:失败 > 终止 > 暂停 > 完成 > 执行中;没有日志时固定为“未执行”。 * 完成状态同时识别状态字段、progress=100 以及 completed/finished 布尔标记。 */ -const statusOf = (logs) => { +const statusOf = (logs, execution = {}) => { if (!logs.length) return { label: '未执行', type: 'info' } const statuses = logs.flatMap((log) => [ @@ -184,6 +209,26 @@ const statusOf = (logs) => { if (statuses.some((status) => ['3', 'paused', 'pause'].includes(status))) { return { label: '已暂停', type: 'warning' } } + const taskStatus = execution.taskStatus === null || execution.taskStatus === undefined || execution.taskStatus === '' + ? Number.NaN + : Number(execution.taskStatus) + if (taskStatus === 0) return { label: '已完成', type: 'success' } + if (taskStatus === 2) { + return execution.groupIndex < execution.lastLoggedIndex + ? { label: '已完成', type: 'success' } + : { label: '执行失败', type: 'danger' } + } + if (taskStatus === 4) { + return execution.groupIndex < execution.lastLoggedIndex + ? { label: '已完成', type: 'success' } + : { label: '已终止', type: 'warning' } + } + if (execution.groupIndex < execution.lastLoggedIndex) { + return { label: '已完成', type: 'success' } + } + if (logs.some(isItemCompletionLog)) { + return { label: '已完成', type: 'success' } + } if ( statuses.some((status) => ['0', 'success', 'succeeded', 'completed', 'complete', 'finished'].includes(status)) || logs.some((log) => Number(log?.progress) >= 100 || log?.completed === true || log?.finished === true) @@ -193,6 +238,49 @@ const statusOf = (logs) => { return { label: '执行中', type: 'primary' } } +const booleanValue = (value) => { + if (value === true || value === false) return value + if (typeof value === 'string') { + if (value.toLowerCase() === 'true') return true + if (value.toLowerCase() === 'false') return false + } + return null +} + +const analysisVerdictOf = (log) => { + const output = { + ...parseObject(log?.actualResult), + ...parseObject(log?.extraInfo), + } + const testStatus = String(log?.testStatus ?? '') + const analysisType = String(output.analysisType || '').toUpperCase() + const profileCode = String(output.profileCode || '').toLowerCase() + const hasAnalysis = Boolean( + analysisType.includes('AUDIO') || analysisType.includes('VIDEO') + || profileCode.includes('audio') || profileCode.includes('video') + || output.analysisStatus + || output.passed !== undefined || output.matched !== undefined + || ['1', '2'].includes(testStatus) + ) + if (!hasAnalysis) return null + const analysisStatus = String(output.analysisStatus || '').toUpperCase() + if (['FAILED', 'ERROR'].includes(analysisStatus)) return false + const passed = booleanValue(output.passed) + if (passed !== null) return passed + const matched = booleanValue(output.matched) + if (matched !== null) return matched + if (testStatus === '1') return true + return false +} + +const analysisStatusOf = (logs) => { + const verdicts = logs.map(analysisVerdictOf).filter((value) => value !== null) + if (!verdicts.length) return null + return verdicts.every(Boolean) + ? { label: '通过', type: 'success' } + : { label: '不通过', type: 'danger' } +} + // 父组件明确传入 items 时直接使用,否则使用 taskId 请求到的检测项。 const taskItemsSource = computed(() => props.items.length ? props.items : taskItems.value) @@ -203,48 +291,100 @@ const taskItemsSource = computed(() => props.items.length ? props.items : taskIt * 3. 最后计算节点名称集合和检测项状态。 */ const groups = computed(() => { + const occurrenceByItemKey = new Map() + const totalByItemKey = new Map() + taskItemsSource.value.forEach((item) => { + const primaryKey = itemKeysOf(item)[0] + if (primaryKey) totalByItemKey.set(primaryKey, (totalByItemKey.get(primaryKey) || 0) + 1) + }) + // 每个检测项先建立空日志数组,所以初始状态能够由 statusOf 正确显示为“未执行”。 - const result = taskItemsSource.value.map((item, index) => ({ - key: itemKeysOf(item)[0] || `item-${index}`, - name: nameOf(item), - logs: [], - })) - const keyToGroup = new Map() + const result = taskItemsSource.value.map((item, index) => { + const primaryKey = itemKeysOf(item)[0] || `item-${index}` + const occurrence = (occurrenceByItemKey.get(primaryKey) || 0) + 1 + occurrenceByItemKey.set(primaryKey, occurrence) + const repeated = (totalByItemKey.get(primaryKey) || 0) > 1 + const repeatLabel = repeated ? `第 ${occurrence} 次` : '' + const name = nameOf(item) + return { + key: `order-${index + 1}:${primaryKey}:${occurrence}`, + name, + fullName: repeatLabel ? `${name}(${repeatLabel})` : name, + itemKeys: itemKeysOf(item), + occurrence, + repeatLabel, + logs: [], + } + }) + const keyToGroups = new Map() // 一个检测项可能同时存在 detectItemId、testCaseId 等多个 ID,全部映射到同一个分组。 taskItemsSource.value.forEach((item, index) => { const group = result[index] itemKeysOf(item).forEach((key) => { - if (!keyToGroup.has(key)) keyToGroup.set(key, group) + if (!keyToGroups.has(key)) keyToGroups.set(key, []) + keyToGroups.get(key).push(group) }) }) const unmatchedLogs = [] props.list.forEach((log) => { - const order = Number(log?.itemOrder) + const executionContext = logExecutionContextOf(log) + const order = Number(executionContext.itemOrder) + const occurrence = Number(executionContext.itemOccurrence) // itemOrder 是任务内的 1 基序号,存在时可以准确处理同一检测项被重复编排的情况。 const orderedGroup = Number.isInteger(order) && order > 0 ? result[order - 1] : null + // Older persisted logs did not contain itemOrder. Their monotonically increasing + // progress still identifies the binding segment and keeps repeated items separate. + const progress = Number(log?.progress) + const progressOrder = Number.isFinite(progress) && progress > 0 && result.length + ? Math.max(1, Math.min(result.length, Math.ceil(progress * result.length / 100))) + : 0 + const progressGroup = progressOrder > 0 ? result[progressOrder - 1] : null + const logKeys = logKeysOf(log) + const compatibleProgressGroup = progressGroup + && logKeys.some((key) => progressGroup.itemKeys.includes(key)) + ? progressGroup + : null + // 没有有效序号时,再使用日志 itemId 与检测项 detectItemId 等字段进行匹配。 - const matchedGroup = orderedGroup || logKeysOf(log).map((key) => keyToGroup.get(key)).find(Boolean) + const matchedGroup = orderedGroup || compatibleProgressGroup || logKeys.map((key) => { + const candidates = keyToGroups.get(key) || [] + return Number.isInteger(occurrence) && occurrence > 0 + ? candidates[occurrence - 1] + : candidates[0] + }).find(Boolean) if (matchedGroup) matchedGroup.logs.push(log) else unmatchedLogs.push(log) }) // 无法关联的日志不能静默丢失,统一放入兜底分组,便于发现后端字段或历史数据问题。 + const lastLoggedIndex = result.reduce((lastIndex, group, index) => ( + group.logs.length ? index : lastIndex + ), -1) + if (unmatchedLogs.length) { result.push({ key: 'unmatched', name: '未关联检测项', + fullName: '未关联检测项', + itemKeys: [], + repeatLabel: '', logs: unmatchedLogs, }) } - return result.map((group) => ({ + return result.map((group, index) => ({ ...group, nodeNames: uniqueValues(group.logs.map(nodeNameOf)), - status: statusOf(group.logs), + status: statusOf(group.logs, { + groupIndex: index, + lastLoggedIndex, + taskStatus: props.taskStatus, + }), + analysisStatus: analysisStatusOf(group.logs), })) }) @@ -379,7 +519,18 @@ watch(() => groups.value.map((group) => group.key), (keys) => { white-space: nowrap; } -.task-log__status { +.task-log__repeat { + display: inline-block; + margin-left: 6px; + color: #409eff; + font-size: 12px; + font-weight: 500; +} + +.task-log__statuses { + display: flex; + flex-wrap: wrap; + gap: 6px; justify-self: end; }