Skip to content

Commit d67259b

Browse files
committed
feat: 流程图悬浮框筛选
1 parent 34a5325 commit d67259b

1 file changed

Lines changed: 133 additions & 23 deletions

File tree

src/views/flowchart/components/FlowchartNodePopover.vue

Lines changed: 133 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
<script setup lang="ts">
2+
import { computed, ref, watch } from 'vue'
23
import { NTag } from 'naive-ui'
34
import type { NodeInfo } from '../../../types'
45
import { 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')">&times;</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

Comments
 (0)