forked from SmartBench/CMVR-IOT-UI
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,
|
AGV_MOVE_TO_STATION: DEVICE_KIND.AGV,
|
||||||
ARM_MOVE_TO_POINT: DEVICE_KIND.ARM,
|
ARM_MOVE_TO_POINT: DEVICE_KIND.ARM,
|
||||||
ARM_MOVE_TO_J: DEVICE_KIND.ARM,
|
ARM_MOVE_TO_J: DEVICE_KIND.ARM,
|
||||||
|
ARM_ENABLE: DEVICE_KIND.ARM,
|
||||||
|
ARM_DISABLE: DEVICE_KIND.ARM,
|
||||||
TOUCH: DEVICE_KIND.ARM,
|
TOUCH: DEVICE_KIND.ARM,
|
||||||
BIO_HEAD_SPEAK_START: DEVICE_KIND.BIO_HEAD,
|
BIO_HEAD_SPEAK_START: DEVICE_KIND.BIO_HEAD,
|
||||||
BIO_HEAD_SPEAK_STOP: DEVICE_KIND.BIO_HEAD,
|
BIO_HEAD_SPEAK_STOP: DEVICE_KIND.BIO_HEAD,
|
||||||
|
|||||||
@ -143,7 +143,7 @@ const analysisOf = (log) => {
|
|||||||
;[
|
;[
|
||||||
'analysisStatus', 'analysisType', 'profileCode', 'analysisModel', 'analysisTimingMs',
|
'analysisStatus', 'analysisType', 'profileCode', 'analysisModel', 'analysisTimingMs',
|
||||||
'mediaUrl', 'audioUrl', 'videoUrl', 'imageUrl', 'label', 'labelName', 'matched',
|
'mediaUrl', 'audioUrl', 'videoUrl', 'imageUrl', 'label', 'labelName', 'matched',
|
||||||
'confidence', 'passed', 'summary', 'conclusion', 'events',
|
'confidence', 'passed', 'summary', 'conclusion', 'events', '_executionContext',
|
||||||
].forEach((key) => delete detailSource[key])
|
].forEach((key) => delete detailSource[key])
|
||||||
const hasDetails = Object.keys(detailSource).length > 0
|
const hasDetails = Object.keys(detailSource).length > 0
|
||||||
const result = {
|
const result = {
|
||||||
|
|||||||
@ -389,6 +389,28 @@ function handler(params) {
|
|||||||
],
|
],
|
||||||
outputType: 'json'
|
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,
|
icon: cameraSvg,
|
||||||
name: "开始监听报警事件",
|
name: "开始监听报警事件",
|
||||||
|
|||||||
@ -506,7 +506,9 @@ const subscribeDebounced = debounce(async (sub) => {
|
|||||||
|
|
||||||
const callback = (data) => {
|
const callback = (data) => {
|
||||||
if (data !== '500') {
|
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] || []
|
const steps = pointList_runningTask.value[data.taskId] || []
|
||||||
let activeStep = Number.isInteger(data.itemOrder) && data.itemOrder > 0
|
let activeStep = Number.isInteger(data.itemOrder) && data.itemOrder > 0
|
||||||
? data.itemOrder - 1
|
? data.itemOrder - 1
|
||||||
|
|||||||
@ -67,7 +67,8 @@
|
|||||||
<div class="run-log-container">
|
<div class="run-log-container">
|
||||||
<div>执行日志</div>
|
<div>执行日志</div>
|
||||||
<!-- 传入任务定义 ID,task-log 会据此查询检测项并按检测项归类当前实例日志。 -->
|
<!-- 传入任务定义 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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -156,7 +157,8 @@
|
|||||||
|
|
||||||
<el-dialog title="任务执行日志" v-model="logDialogVisible" width="800px" append-to-body>
|
<el-dialog title="任务执行日志" v-model="logDialogVisible" width="800px" append-to-body>
|
||||||
<!-- 历史弹窗也传入对应任务定义 ID,确保没有实时进度数据时仍能展示完整检测项列表。 -->
|
<!-- 历史弹窗也传入对应任务定义 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>
|
||||||
|
|
||||||
<el-dialog title="本次执行安排" v-model="startDialogVisible" width="820px" append-to-body>
|
<el-dialog title="本次执行安排" v-model="startDialogVisible" width="820px" append-to-body>
|
||||||
@ -550,7 +552,9 @@ const subscribeDebounced = debounce(async (sub) => {
|
|||||||
|
|
||||||
const callback = (data) => {
|
const callback = (data) => {
|
||||||
if (data !== '500') {
|
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] || []
|
const steps = pointList_runningTask.value[data.taskId] || []
|
||||||
let activeStep = Number.isInteger(data.itemOrder) && data.itemOrder > 0
|
let activeStep = Number.isInteger(data.itemOrder) && data.itemOrder > 0
|
||||||
? data.itemOrder - 1
|
? data.itemOrder - 1
|
||||||
@ -595,6 +599,7 @@ const logDialogData = ref([])
|
|||||||
|
|
||||||
// 历史表格行里的 taskId 是任务定义 ID;日志组件使用它查询本次任务包含的全部检测项。
|
// 历史表格行里的 taskId 是任务定义 ID;日志组件使用它查询本次任务包含的全部检测项。
|
||||||
const logDialogTaskId = ref('')
|
const logDialogTaskId = ref('')
|
||||||
|
const logDialogTaskStatus = ref(null)
|
||||||
const logDialogLoading = ref(false)
|
const logDialogLoading = ref(false)
|
||||||
let logDialogRequestId = 0
|
let logDialogRequestId = 0
|
||||||
const handleOpenLog = async (row) => {
|
const handleOpenLog = async (row) => {
|
||||||
@ -603,6 +608,7 @@ const handleOpenLog = async (row) => {
|
|||||||
|
|
||||||
// 在请求实例日志前先更新 taskId,让检测项请求与日志请求并行执行,减少弹窗等待时间。
|
// 在请求实例日志前先更新 taskId,让检测项请求与日志请求并行执行,减少弹窗等待时间。
|
||||||
logDialogTaskId.value = row.taskId
|
logDialogTaskId.value = row.taskId
|
||||||
|
logDialogTaskStatus.value = row.status
|
||||||
logDialogData.value = []
|
logDialogData.value = []
|
||||||
logDialogLoading.value = true
|
logDialogLoading.value = true
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -21,10 +21,16 @@
|
|||||||
<ArrowRight />
|
<ArrowRight />
|
||||||
</el-icon>
|
</el-icon>
|
||||||
<span class="task-log__order">{{ String(index + 1).padStart(2, '0') }}</span>
|
<span class="task-log__order">{{ String(index + 1).padStart(2, '0') }}</span>
|
||||||
<span class="task-log__name" :title="group.name">{{ group.name }}</span>
|
<span class="task-log__name" :title="group.fullName">
|
||||||
<el-tag class="task-log__status" :type="group.status.type" size="small">
|
{{ group.name }}
|
||||||
{{ group.status.label }}
|
<span v-if="group.repeatLabel" class="task-log__repeat">{{ group.repeatLabel }}</span>
|
||||||
</el-tag>
|
</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>
|
</button>
|
||||||
|
|
||||||
<!-- 展开后只把当前检测项已经归类的节点日志交给通用日志组件渲染。 -->
|
<!-- 展开后只把当前检测项已经归类的节点日志交给通用日志组件渲染。 -->
|
||||||
@ -62,6 +68,7 @@ import TaskExecutionLog from '@/views/flow/components/TaskExecutionLog.vue'
|
|||||||
*/
|
*/
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
taskId: { type: [String, Number], default: '' },
|
taskId: { type: [String, Number], default: '' },
|
||||||
|
taskStatus: { type: [String, Number], default: null },
|
||||||
list: { type: Array, default: () => [] },
|
list: { type: Array, default: () => [] },
|
||||||
items: { type: Array, default: () => [] },
|
items: { type: Array, default: () => [] },
|
||||||
height: { type: Number, default: 150 },
|
height: { type: Number, default: 150 },
|
||||||
@ -120,6 +127,7 @@ const itemKeysOf = (item) => uniqueValues([
|
|||||||
const logKeysOf = (log) => {
|
const logKeysOf = (log) => {
|
||||||
const actualResult = parseObject(log?.actualResult)
|
const actualResult = parseObject(log?.actualResult)
|
||||||
const extraInfo = parseObject(log?.extraInfo)
|
const extraInfo = parseObject(log?.extraInfo)
|
||||||
|
const executionContext = parseObject(actualResult?._executionContext)
|
||||||
return uniqueValues([
|
return uniqueValues([
|
||||||
log?.itemId,
|
log?.itemId,
|
||||||
log?.detectItemId,
|
log?.detectItemId,
|
||||||
@ -130,11 +138,21 @@ const logKeysOf = (log) => {
|
|||||||
log?.item?.detectItemId,
|
log?.item?.detectItemId,
|
||||||
actualResult.itemId,
|
actualResult.itemId,
|
||||||
actualResult.detectItemId,
|
actualResult.detectItemId,
|
||||||
|
executionContext.itemId,
|
||||||
extraInfo.itemId,
|
extraInfo.itemId,
|
||||||
extraInfo.detectItemId,
|
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 || '未命名检测项'
|
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 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 布尔标记。
|
* 完成状态同时识别状态字段、progress=100 以及 completed/finished 布尔标记。
|
||||||
*/
|
*/
|
||||||
const statusOf = (logs) => {
|
const statusOf = (logs, execution = {}) => {
|
||||||
if (!logs.length) return { label: '未执行', type: 'info' }
|
if (!logs.length) return { label: '未执行', type: 'info' }
|
||||||
|
|
||||||
const statuses = logs.flatMap((log) => [
|
const statuses = logs.flatMap((log) => [
|
||||||
@ -184,6 +209,26 @@ const statusOf = (logs) => {
|
|||||||
if (statuses.some((status) => ['3', 'paused', 'pause'].includes(status))) {
|
if (statuses.some((status) => ['3', 'paused', 'pause'].includes(status))) {
|
||||||
return { label: '已暂停', type: 'warning' }
|
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 (
|
if (
|
||||||
statuses.some((status) => ['0', 'success', 'succeeded', 'completed', 'complete', 'finished'].includes(status))
|
statuses.some((status) => ['0', 'success', 'succeeded', 'completed', 'complete', 'finished'].includes(status))
|
||||||
|| logs.some((log) => Number(log?.progress) >= 100 || log?.completed === true || log?.finished === true)
|
|| logs.some((log) => Number(log?.progress) >= 100 || log?.completed === true || log?.finished === true)
|
||||||
@ -193,6 +238,49 @@ const statusOf = (logs) => {
|
|||||||
return { label: '执行中', type: 'primary' }
|
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 请求到的检测项。
|
// 父组件明确传入 items 时直接使用,否则使用 taskId 请求到的检测项。
|
||||||
const taskItemsSource = computed(() => props.items.length ? props.items : taskItems.value)
|
const taskItemsSource = computed(() => props.items.length ? props.items : taskItems.value)
|
||||||
|
|
||||||
@ -203,48 +291,100 @@ const taskItemsSource = computed(() => props.items.length ? props.items : taskIt
|
|||||||
* 3. 最后计算节点名称集合和检测项状态。
|
* 3. 最后计算节点名称集合和检测项状态。
|
||||||
*/
|
*/
|
||||||
const groups = computed(() => {
|
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 正确显示为“未执行”。
|
// 每个检测项先建立空日志数组,所以初始状态能够由 statusOf 正确显示为“未执行”。
|
||||||
const result = taskItemsSource.value.map((item, index) => ({
|
const result = taskItemsSource.value.map((item, index) => {
|
||||||
key: itemKeysOf(item)[0] || `item-${index}`,
|
const primaryKey = itemKeysOf(item)[0] || `item-${index}`
|
||||||
name: nameOf(item),
|
const occurrence = (occurrenceByItemKey.get(primaryKey) || 0) + 1
|
||||||
logs: [],
|
occurrenceByItemKey.set(primaryKey, occurrence)
|
||||||
}))
|
const repeated = (totalByItemKey.get(primaryKey) || 0) > 1
|
||||||
const keyToGroup = new Map()
|
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,全部映射到同一个分组。
|
// 一个检测项可能同时存在 detectItemId、testCaseId 等多个 ID,全部映射到同一个分组。
|
||||||
taskItemsSource.value.forEach((item, index) => {
|
taskItemsSource.value.forEach((item, index) => {
|
||||||
const group = result[index]
|
const group = result[index]
|
||||||
itemKeysOf(item).forEach((key) => {
|
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 = []
|
const unmatchedLogs = []
|
||||||
props.list.forEach((log) => {
|
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 基序号,存在时可以准确处理同一检测项被重复编排的情况。
|
// itemOrder 是任务内的 1 基序号,存在时可以准确处理同一检测项被重复编排的情况。
|
||||||
const orderedGroup = Number.isInteger(order) && order > 0 ? result[order - 1] : null
|
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 等字段进行匹配。
|
// 没有有效序号时,再使用日志 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)
|
if (matchedGroup) matchedGroup.logs.push(log)
|
||||||
else unmatchedLogs.push(log)
|
else unmatchedLogs.push(log)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 无法关联的日志不能静默丢失,统一放入兜底分组,便于发现后端字段或历史数据问题。
|
// 无法关联的日志不能静默丢失,统一放入兜底分组,便于发现后端字段或历史数据问题。
|
||||||
|
const lastLoggedIndex = result.reduce((lastIndex, group, index) => (
|
||||||
|
group.logs.length ? index : lastIndex
|
||||||
|
), -1)
|
||||||
|
|
||||||
if (unmatchedLogs.length) {
|
if (unmatchedLogs.length) {
|
||||||
result.push({
|
result.push({
|
||||||
key: 'unmatched',
|
key: 'unmatched',
|
||||||
name: '未关联检测项',
|
name: '未关联检测项',
|
||||||
|
fullName: '未关联检测项',
|
||||||
|
itemKeys: [],
|
||||||
|
repeatLabel: '',
|
||||||
logs: unmatchedLogs,
|
logs: unmatchedLogs,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.map((group) => ({
|
return result.map((group, index) => ({
|
||||||
...group,
|
...group,
|
||||||
nodeNames: uniqueValues(group.logs.map(nodeNameOf)),
|
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;
|
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;
|
justify-self: end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user