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;
}