11<script setup lang="ts">
2+ import { computed , ref , watch } from ' vue'
23import { NTag } from ' naive-ui'
34import type { NodeInfo } from ' ../../../types'
45import { convertFileSrc } from ' ../utils/nodeImageLookup'
@@ -10,7 +11,7 @@ interface FlowchartPopoverData {
1011 executionOrder? : number []
1112}
1213
13- defineProps <{
14+ const props = defineProps <{
1415 visible: boolean
1516 position: { x: number ; y: number }
1617 popoverData: FlowchartPopoverData | null
@@ -23,6 +24,62 @@ const emit = defineEmits<{
2324 close: []
2425 ' navigate-to-node' : [node : NodeInfo ]
2526}>()
27+
28+ interface PopoverExecutionEntry {
29+ info: NodeInfo
30+ executionOrder: number
31+ }
32+
33+ const showAbnormalOnly = ref (false )
34+
35+ const hasFailedAction = (info : NodeInfo ): boolean => {
36+ if (info .action_details && info .action_details .success === false ) return true
37+ return (info .node_flow || []).some ((item ) => item .type === ' action' && item .status === ' failed' )
38+ }
39+
40+ const isAbnormalExecution = (info : NodeInfo ): boolean => {
41+ return info .status === ' failed' || hasFailedAction (info )
42+ }
43+
44+ const hasAbnormalEntries = computed (() => {
45+ const data = props .popoverData
46+ if (! data ) return false
47+ return data .nodeInfos .some ((info ) => isAbnormalExecution (info ))
48+ })
49+
50+ const popoverEntries = computed <PopoverExecutionEntry []>(() => {
51+ const data = props .popoverData
52+ if (! data ) return []
53+
54+ const entries = data .nodeInfos .map ((info , idx ) => ({
55+ info ,
56+ executionOrder: data .executionOrder ?.[idx ] ?? idx + 1 ,
57+ }))
58+
59+ if (! showAbnormalOnly .value ) return entries
60+ return entries .filter ((entry ) => isAbnormalExecution (entry .info ))
61+ })
62+
63+ const toggleAbnormalOnly = () => {
64+ if (! hasAbnormalEntries .value ) return
65+ showAbnormalOnly .value = ! showAbnormalOnly .value
66+ }
67+
68+ watch (
69+ () => props .popoverData ,
70+ () => {
71+ showAbnormalOnly .value = false
72+ }
73+ )
74+
75+ watch (
76+ () => props .visible ,
77+ (visible ) => {
78+ if (! visible ) {
79+ showAbnormalOnly .value = false
80+ }
81+ }
82+ )
2683 </script >
2784
2885<template >
@@ -33,44 +90,56 @@ const emit = defineEmits<{
3390 >
3491 <div class =" popover-header" >
3592 <span class =" popover-title" >{{ popoverData.label }}</span >
93+ <button
94+ class =" popover-filter-btn"
95+ type =" button"
96+ :title =" showAbnormalOnly ? '仅显示异常执行(点击显示全部)' : '显示全部执行(点击仅异常)'"
97+ :disabled =" !hasAbnormalEntries"
98+ @click =" toggleAbnormalOnly"
99+ >
100+ <span class =" popover-filter-dot" :class =" { 'popover-filter-dot--active': showAbnormalOnly }" />
101+ </button >
36102 <span class =" popover-close" @click =" emit('close')" >× ; </span >
37103 </div >
38104 <div class =" popover-body" >
105+ <div v-if =" popoverEntries.length === 0" class =" popover-empty" >
106+ 暂无异常执行
107+ </div >
39108 <div
40- v-for =" (info , idx) in popoverData.nodeInfos "
41- :key =" info.node_id"
109+ v-for =" (entry , idx) in popoverEntries "
110+ :key =" `${entry.executionOrder}-${entry. info.node_id}-${idx}` "
42111 >
43112 <div v-if =" popoverData.nodeInfos.length > 1" class =" popover-exec-label" >
44- 执行 #{{ popoverData .executionOrder?.[idx] ?? idx + 1 }}
113+ 执行 #{{ entry .executionOrder }}
45114 </div >
46115 <div class =" popover-row" >
47- <n-tag size =" tiny" :type =" getRuntimeStatusTagType(info.status)" >
48- {{ getRuntimeStatusText(info.status) }}
116+ <n-tag size =" tiny" :type =" getRuntimeStatusTagType(entry. info.status)" >
117+ {{ getRuntimeStatusText(entry. info.status) }}
49118 </n-tag >
50- <span class =" popover-time" >{{ info.ts }}</span >
51- <span class =" popover-locate" @click =" emit('navigate-to-node', info)" >定位</span >
119+ <span class =" popover-time" >{{ entry. info.ts }}</span >
120+ <span class =" popover-locate" @click =" emit('navigate-to-node', entry. info)" >定位</span >
52121 </div >
53- <div v-if =" info.reco_details" class =" popover-row" >
122+ <div v-if =" entry. info.reco_details" class =" popover-row" >
54123 <span class =" popover-label" >识别</span >
55- <span >{{ info.reco_details.algorithm }}</span >
56- <span v-if =" info.reco_details.box" class =" popover-secondary" >
57- [{{ info.reco_details.box.join(', ') }}]
124+ <span >{{ entry. info.reco_details.algorithm }}</span >
125+ <span v-if =" entry. info.reco_details.box" class =" popover-secondary" >
126+ [{{ entry. info.reco_details.box.join(', ') }}]
58127 </span >
59128 </div >
60- <div v-if =" info.action_details" class =" popover-row" >
129+ <div v-if =" entry. info.action_details" class =" popover-row" >
61130 <span class =" popover-label" >动作</span >
62- <span >{{ info.action_details.action }}</span >
63- <n-tag size =" tiny" :type =" info.status === 'running' ? 'warning' : info.action_details.success ? 'success' : 'error'" style =" margin-left : 4px " >
64- {{ info.status === 'running' ? getRuntimeStatusText(info.status) : info.action_details.success ? '成功' : '失败' }}
131+ <span >{{ entry. info.action_details.action }}</span >
132+ <n-tag size =" tiny" :type =" entry. info.status === 'running' ? 'warning' : entry. info.action_details.success ? 'success' : 'error'" style =" margin-left : 4px " >
133+ {{ entry. info.status === 'running' ? getRuntimeStatusText(entry. info.status) : entry. info.action_details.success ? '成功' : '失败' }}
65134 </n-tag >
66135 </div >
67136 <safe-preview-image
68- v-if =" nodeImageMap.get(info.node_id)"
69- :src =" convertFileSrc(nodeImageMap.get(info.node_id)!)"
137+ v-if =" nodeImageMap.get(entry. info.node_id)"
138+ :src =" convertFileSrc(nodeImageMap.get(entry. info.node_id)!)"
70139 class =" popover-img"
71140 />
72141 <div
73- v-if =" idx < popoverData.nodeInfos .length - 1"
142+ v-if =" idx < popoverEntries .length - 1"
74143 class =" popover-divider"
75144 />
76145 </div >
@@ -105,13 +174,47 @@ const emit = defineEmits<{
105174.popover-title {
106175 font-weight : 600 ;
107176 font-size : 13px ;
108- overflow : hidden ;
109- text-overflow : ellipsis ;
110- white-space : nowrap ;
111- flex : 1 ;
177+ overflow : visible ;
178+ text-overflow : clip ;
179+ white-space : normal ;
180+ overflow-wrap : anywhere;
181+ word-break : break-word ;
182+ line-height : 1.25 ;
183+ flex : 1 1 auto ;
112184 min-width : 0 ;
113185}
114186
187+ .popover-filter-btn {
188+ display : inline-flex ;
189+ align-items : center ;
190+ justify-content : center ;
191+ width : 16px ;
192+ height : 16px ;
193+ padding : 0 ;
194+ border : none ;
195+ background : transparent ;
196+ cursor : pointer ;
197+ margin : 0 8px ;
198+ flex-shrink : 0 ;
199+ }
200+
201+ .popover-filter-btn :disabled {
202+ cursor : default ;
203+ opacity : 0.45 ;
204+ }
205+
206+ .popover-filter-dot {
207+ display : inline-block ;
208+ width : 9px ;
209+ height : 9px ;
210+ border-radius : 50% ;
211+ background : #8f8f8f ;
212+ }
213+
214+ .popover-filter-dot--active {
215+ background : #d03050 ;
216+ }
217+
115218.popover-close {
116219 cursor : pointer ;
117220 font-size : 18px ;
@@ -132,6 +235,13 @@ const emit = defineEmits<{
132235 min-height : 0 ;
133236}
134237
238+ .popover-empty {
239+ color : var (--flowchart-popover-secondary );
240+ font-size : 12px ;
241+ text-align : center ;
242+ padding : 10px 0 ;
243+ }
244+
135245.popover-exec-label {
136246 font-weight : 600 ;
137247 font-size : 11px ;
0 commit comments