lxl-dev #3

Merged
zhanghao merged 122 commits from lxl-dev into dev 2026-08-24 13:55:18 +08:00
6 changed files with 205 additions and 22 deletions
Showing only changes of commit f944d33d58 - Show all commits

View File

@ -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,

View File

@ -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 = {

View File

@ -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: "开始监听报警事件",

View File

@ -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

View File

@ -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 {

View File

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