@ -21,10 +21,16 @@
< ArrowRight / >
< / e l - i c o n >
< 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 } }
< / e l - t a g >
< 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 } } < / e l - t a g >
< el -tag v-if ="group.analysisStatus" :type="group.analysisStatus.type" effect="plain" size="small" >
{ { group . analysisStatus . label } }
< / e l - t a g >
< / 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' }
}
/ / 父 组 件 明 确 传 入 i t e m s 时 直 接 使 用 , 否 则 使 用 t a s k I d 请 求 到 的 检 测 项 。
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 )
} )
/ / 每 个 检 测 项 先 建 立 空 日 志 数 组 , 所 以 初 始 状 态 能 够 由 s t a t u s O f 正 确 显 示 为 “ 未 执 行 ” 。
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 ( )
/ / 一 个 检 测 项 可 能 同 时 存 在 d e t e c t I t e m I d 、 t e s t C a s e I d 等 多 个 I D , 全 部 映 射 到 同 一 个 分 组 。
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 )
/ / i t e m O r d e r 是 任 务 内 的 1 基 序 号 , 存 在 时 可 以 准 确 处 理 同 一 检 测 项 被 重 复 编 排 的 情 况 。
const orderedGroup = Number . isInteger ( order ) && order > 0 ? result [ order - 1 ] : null
/ / O l d e r p e r s i s t e d l o g s d i d n o t c o n t a i n i t e m O r d e r . T h e i r m o n o t o n i c a l l y i n c r e a s i n g
/ / p r o g r e s s s t i l l i d e n t i f i e s t h e b i n d i n g s e g m e n t a n d k e e p s r e p e a t e d i t e m s s e p a r a t e .
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
/ / 没 有 有 效 序 号 时 , 再 使 用 日 志 i t e m I d 与 检 测 项 d e t e c t I t e m I d 等 字 段 进 行 匹 配 。
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 : 6 px ;
color : # 409 eff ;
font - size : 12 px ;
font - weight : 500 ;
}
. task - log _ _statuses {
display : flex ;
flex - wrap : wrap ;
gap : 6 px ;
justify - self : end ;
}