feat(flow): 添加机械臂使能控制节点并优化任务执行日志显示
- 新增开启机械臂使能和关闭机械臂使能两个服务节点 - 在机器人设备工具类中注册ARM_ENABLE和ARM_DISABLE操作类型 - 优化运行任务页面中的状态判断逻辑,支持多种终端状态 - 为任务日志组件增加任务状态属性传递 - 增强日志组件中检测项状态推导功能,支持重复执行和分析状态显示 - 添加执行上下文解析以正确处理重复检测项的日志归类 - 改进日志详情过滤,排除执行上下文相关字段 - 实现检测项完成状态和分析结果的正确标识与展示
This commit is contained in:
parent
43b07c94f4
commit
f944d33d58
@ -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,
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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: "开始监听报警事件",
|
||||
|
||||
@ -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
|
||||
|
||||
@ -67,7 +67,8 @@
|
||||
<div class="run-log-container">
|
||||
<div>执行日志</div>
|
||||
<!-- 传入任务定义 ID,task-log 会据此查询检测项并按检测项归类当前实例日志。 -->
|
||||
<taskLog :height="460" :task-id="runTask.taskId" :list="taskLogList[runTask.id]" />
|
||||
<taskLog :height="460" :task-id="runTask.taskId" :task-status="runTask.status"
|
||||
:list="taskLogList[runTask.id]" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -156,7 +157,8 @@
|
||||
|
||||
<el-dialog title="任务执行日志" v-model="logDialogVisible" width="800px" append-to-body>
|
||||
<!-- 历史弹窗也传入对应任务定义 ID,确保没有实时进度数据时仍能展示完整检测项列表。 -->
|
||||
<taskLog v-loading="logDialogLoading" :task-id="logDialogTaskId" :list="logDialogData" :height="500" />
|
||||
<taskLog v-loading="logDialogLoading" :task-id="logDialogTaskId" :task-status="logDialogTaskStatus"
|
||||
:list="logDialogData" :height="500" />
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog title="本次执行安排" v-model="startDialogVisible" width="820px" append-to-body>
|
||||
@ -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 {
|
||||
|
||||
@ -21,10 +21,16 @@
|
||||
<ArrowRight />
|
||||
</el-icon>
|
||||
<span class="task-log__order">{{ String(index + 1).padStart(2, '0') }}</span>
|
||||
<span class="task-log__name" :title="group.name">{{ group.name }}</span>
|
||||
<el-tag class="task-log__status" :type="group.status.type" size="small">
|
||||
{{ group.status.label }}
|
||||
</el-tag>
|
||||
<span class="task-log__name" :title="group.fullName">
|
||||
{{ group.name }}
|
||||
<span v-if="group.repeatLabel" class="task-log__repeat">{{ group.repeatLabel }}</span>
|
||||
</span>
|
||||
<span class="task-log__statuses">
|
||||
<el-tag :type="group.status.type" size="small">{{ group.status.label }}</el-tag>
|
||||
<el-tag v-if="group.analysisStatus" :type="group.analysisStatus.type" effect="plain" size="small">
|
||||
{{ group.analysisStatus.label }}
|
||||
</el-tag>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<!-- 展开后只把当前检测项已经归类的节点日志交给通用日志组件渲染。 -->
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user