工作流动画效果
This commit is contained in:
@@ -78,21 +78,82 @@ export const StartNode = defineComponent({
|
||||
height: '8px'
|
||||
}
|
||||
}),
|
||||
// 状态指示器(右上角)
|
||||
isExecuting ? h('div', {
|
||||
style: {
|
||||
position: 'absolute',
|
||||
top: '-8px',
|
||||
right: '-8px',
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
background: '#409eff',
|
||||
borderRadius: '50%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
boxShadow: '0 2px 8px rgba(64, 158, 255, 0.6)',
|
||||
zIndex: 3001, // 确保在模态框之上(Element Plus 模态框 z-index 通常是 2000-2100)
|
||||
}
|
||||
}, [
|
||||
h('div', {
|
||||
class: 'node-loading-spinner',
|
||||
style: {
|
||||
width: '12px',
|
||||
height: '12px',
|
||||
border: '2px solid rgba(255, 255, 255, 0.3)',
|
||||
borderTop: '2px solid white',
|
||||
borderRadius: '50%',
|
||||
animation: 'spin 0.8s linear infinite'
|
||||
}
|
||||
})
|
||||
]) : null,
|
||||
isExecuted ? h('div', {
|
||||
style: {
|
||||
position: 'absolute',
|
||||
top: '-8px',
|
||||
right: '-8px',
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
background: '#67c23a',
|
||||
borderRadius: '50%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
boxShadow: '0 2px 8px rgba(103, 194, 58, 0.6)',
|
||||
zIndex: 3001, // 确保在模态框之上(Element Plus 模态框 z-index 通常是 2000-2100)
|
||||
fontSize: '12px',
|
||||
color: 'white'
|
||||
}
|
||||
}, '✓') : null,
|
||||
isFailed ? h('div', {
|
||||
style: {
|
||||
position: 'absolute',
|
||||
top: '-8px',
|
||||
right: '-8px',
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
background: '#f56c6c',
|
||||
borderRadius: '50%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
boxShadow: '0 2px 8px rgba(245, 108, 108, 0.6)',
|
||||
zIndex: 3001, // 确保在模态框之上(Element Plus 模态框 z-index 通常是 2000-2100)
|
||||
fontSize: '12px',
|
||||
color: 'white'
|
||||
}
|
||||
}, '✕') : null,
|
||||
h('div', {
|
||||
style: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '4px'
|
||||
gap: '4px',
|
||||
position: 'relative',
|
||||
zIndex: 1
|
||||
}
|
||||
}, [
|
||||
props.data.label || '开始',
|
||||
isFailed ? h('span', {
|
||||
style: {
|
||||
fontSize: '12px',
|
||||
marginLeft: '4px'
|
||||
}
|
||||
}, '❌') : null
|
||||
props.data.label || '开始'
|
||||
])
|
||||
])
|
||||
}
|
||||
@@ -171,21 +232,82 @@ export const LLMNode = defineComponent({
|
||||
height: '8px'
|
||||
}
|
||||
}),
|
||||
// 状态指示器(右上角)
|
||||
isExecuting ? h('div', {
|
||||
style: {
|
||||
position: 'absolute',
|
||||
top: '-8px',
|
||||
right: '-8px',
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
background: '#409eff',
|
||||
borderRadius: '50%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
boxShadow: '0 2px 8px rgba(64, 158, 255, 0.6)',
|
||||
zIndex: 3001, // 确保在模态框之上(Element Plus 模态框 z-index 通常是 2000-2100)
|
||||
}
|
||||
}, [
|
||||
h('div', {
|
||||
class: 'node-loading-spinner',
|
||||
style: {
|
||||
width: '12px',
|
||||
height: '12px',
|
||||
border: '2px solid rgba(255, 255, 255, 0.3)',
|
||||
borderTop: '2px solid white',
|
||||
borderRadius: '50%',
|
||||
animation: 'spin 0.8s linear infinite'
|
||||
}
|
||||
})
|
||||
]) : null,
|
||||
isExecuted ? h('div', {
|
||||
style: {
|
||||
position: 'absolute',
|
||||
top: '-8px',
|
||||
right: '-8px',
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
background: '#67c23a',
|
||||
borderRadius: '50%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
boxShadow: '0 2px 8px rgba(103, 194, 58, 0.6)',
|
||||
zIndex: 3001, // 确保在模态框之上(Element Plus 模态框 z-index 通常是 2000-2100)
|
||||
fontSize: '12px',
|
||||
color: 'white'
|
||||
}
|
||||
}, '✓') : null,
|
||||
isFailed ? h('div', {
|
||||
style: {
|
||||
position: 'absolute',
|
||||
top: '-8px',
|
||||
right: '-8px',
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
background: '#f56c6c',
|
||||
borderRadius: '50%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
boxShadow: '0 2px 8px rgba(245, 108, 108, 0.6)',
|
||||
zIndex: 3001, // 确保在模态框之上(Element Plus 模态框 z-index 通常是 2000-2100)
|
||||
fontSize: '12px',
|
||||
color: 'white'
|
||||
}
|
||||
}, '✕') : null,
|
||||
h('div', {
|
||||
style: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '4px'
|
||||
gap: '4px',
|
||||
position: 'relative',
|
||||
zIndex: 1
|
||||
}
|
||||
}, [
|
||||
props.data.label || 'LLM',
|
||||
isFailed ? h('span', {
|
||||
style: {
|
||||
fontSize: '12px',
|
||||
marginLeft: '4px'
|
||||
}
|
||||
}, '❌') : null
|
||||
props.data.label || 'LLM'
|
||||
]),
|
||||
h(Handle, {
|
||||
type: 'source',
|
||||
@@ -282,7 +404,77 @@ export const ConditionNode = defineComponent({
|
||||
height: '8px'
|
||||
}
|
||||
}),
|
||||
props.data.label || '条件',
|
||||
// 状态指示器(右上角)
|
||||
isExecuting ? h('div', {
|
||||
style: {
|
||||
position: 'absolute',
|
||||
top: '-8px',
|
||||
right: '-8px',
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
background: '#409eff',
|
||||
borderRadius: '50%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
boxShadow: '0 2px 8px rgba(64, 158, 255, 0.6)',
|
||||
zIndex: 3001, // 确保在模态框之上(Element Plus 模态框 z-index 通常是 2000-2100)
|
||||
}
|
||||
}, [
|
||||
h('div', {
|
||||
class: 'node-loading-spinner',
|
||||
style: {
|
||||
width: '12px',
|
||||
height: '12px',
|
||||
border: '2px solid rgba(255, 255, 255, 0.3)',
|
||||
borderTop: '2px solid white',
|
||||
borderRadius: '50%',
|
||||
animation: 'spin 0.8s linear infinite'
|
||||
}
|
||||
})
|
||||
]) : null,
|
||||
isExecuted ? h('div', {
|
||||
style: {
|
||||
position: 'absolute',
|
||||
top: '-8px',
|
||||
right: '-8px',
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
background: '#67c23a',
|
||||
borderRadius: '50%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
boxShadow: '0 2px 8px rgba(103, 194, 58, 0.6)',
|
||||
zIndex: 3001, // 确保在模态框之上(Element Plus 模态框 z-index 通常是 2000-2100)
|
||||
fontSize: '12px',
|
||||
color: 'white'
|
||||
}
|
||||
}, '✓') : null,
|
||||
isFailed ? h('div', {
|
||||
style: {
|
||||
position: 'absolute',
|
||||
top: '-8px',
|
||||
right: '-8px',
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
background: '#f56c6c',
|
||||
borderRadius: '50%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
boxShadow: '0 2px 8px rgba(245, 108, 108, 0.6)',
|
||||
zIndex: 3001, // 确保在模态框之上(Element Plus 模态框 z-index 通常是 2000-2100)
|
||||
fontSize: '12px',
|
||||
color: 'white'
|
||||
}
|
||||
}, '✕') : null,
|
||||
h('div', {
|
||||
style: {
|
||||
position: 'relative',
|
||||
zIndex: 1
|
||||
}
|
||||
}, props.data.label || '条件'),
|
||||
h(Handle, {
|
||||
type: 'source',
|
||||
position: Position.Bottom,
|
||||
@@ -390,7 +582,77 @@ export const EndNode = defineComponent({
|
||||
height: '8px'
|
||||
}
|
||||
}),
|
||||
props.data.label || '结束'
|
||||
// 状态指示器(右上角)
|
||||
isExecuting ? h('div', {
|
||||
style: {
|
||||
position: 'absolute',
|
||||
top: '-8px',
|
||||
right: '-8px',
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
background: '#409eff',
|
||||
borderRadius: '50%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
boxShadow: '0 2px 8px rgba(64, 158, 255, 0.6)',
|
||||
zIndex: 3001, // 确保在模态框之上(Element Plus 模态框 z-index 通常是 2000-2100)
|
||||
}
|
||||
}, [
|
||||
h('div', {
|
||||
class: 'node-loading-spinner',
|
||||
style: {
|
||||
width: '12px',
|
||||
height: '12px',
|
||||
border: '2px solid rgba(255, 255, 255, 0.3)',
|
||||
borderTop: '2px solid white',
|
||||
borderRadius: '50%',
|
||||
animation: 'spin 0.8s linear infinite'
|
||||
}
|
||||
})
|
||||
]) : null,
|
||||
isExecuted ? h('div', {
|
||||
style: {
|
||||
position: 'absolute',
|
||||
top: '-8px',
|
||||
right: '-8px',
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
background: '#67c23a',
|
||||
borderRadius: '50%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
boxShadow: '0 2px 8px rgba(103, 194, 58, 0.6)',
|
||||
zIndex: 3001, // 确保在模态框之上(Element Plus 模态框 z-index 通常是 2000-2100)
|
||||
fontSize: '12px',
|
||||
color: 'white'
|
||||
}
|
||||
}, '✓') : null,
|
||||
isFailed ? h('div', {
|
||||
style: {
|
||||
position: 'absolute',
|
||||
top: '-8px',
|
||||
right: '-8px',
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
background: '#f56c6c',
|
||||
borderRadius: '50%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
boxShadow: '0 2px 8px rgba(245, 108, 108, 0.6)',
|
||||
zIndex: 3001, // 确保在模态框之上(Element Plus 模态框 z-index 通常是 2000-2100)
|
||||
fontSize: '12px',
|
||||
color: 'white'
|
||||
}
|
||||
}, '✕') : null,
|
||||
h('div', {
|
||||
style: {
|
||||
position: 'relative',
|
||||
zIndex: 1
|
||||
}
|
||||
}, props.data.label || '结束')
|
||||
])
|
||||
}
|
||||
}
|
||||
@@ -466,7 +728,77 @@ export const DefaultNode = defineComponent({
|
||||
height: '8px'
|
||||
}
|
||||
}),
|
||||
props.data.label || '节点',
|
||||
// 状态指示器(右上角)
|
||||
isExecuting ? h('div', {
|
||||
style: {
|
||||
position: 'absolute',
|
||||
top: '-8px',
|
||||
right: '-8px',
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
background: '#409eff',
|
||||
borderRadius: '50%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
boxShadow: '0 2px 8px rgba(64, 158, 255, 0.6)',
|
||||
zIndex: 3001, // 确保在模态框之上(Element Plus 模态框 z-index 通常是 2000-2100)
|
||||
}
|
||||
}, [
|
||||
h('div', {
|
||||
class: 'node-loading-spinner',
|
||||
style: {
|
||||
width: '12px',
|
||||
height: '12px',
|
||||
border: '2px solid rgba(255, 255, 255, 0.3)',
|
||||
borderTop: '2px solid white',
|
||||
borderRadius: '50%',
|
||||
animation: 'spin 0.8s linear infinite'
|
||||
}
|
||||
})
|
||||
]) : null,
|
||||
isExecuted ? h('div', {
|
||||
style: {
|
||||
position: 'absolute',
|
||||
top: '-8px',
|
||||
right: '-8px',
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
background: '#67c23a',
|
||||
borderRadius: '50%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
boxShadow: '0 2px 8px rgba(103, 194, 58, 0.6)',
|
||||
zIndex: 3001, // 确保在模态框之上(Element Plus 模态框 z-index 通常是 2000-2100)
|
||||
fontSize: '12px',
|
||||
color: 'white'
|
||||
}
|
||||
}, '✓') : null,
|
||||
isFailed ? h('div', {
|
||||
style: {
|
||||
position: 'absolute',
|
||||
top: '-8px',
|
||||
right: '-8px',
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
background: '#f56c6c',
|
||||
borderRadius: '50%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
boxShadow: '0 2px 8px rgba(245, 108, 108, 0.6)',
|
||||
zIndex: 3001, // 确保在模态框之上(Element Plus 模态框 z-index 通常是 2000-2100)
|
||||
fontSize: '12px',
|
||||
color: 'white'
|
||||
}
|
||||
}, '✕') : null,
|
||||
h('div', {
|
||||
style: {
|
||||
position: 'relative',
|
||||
zIndex: 1
|
||||
}
|
||||
}, props.data.label || '节点'),
|
||||
h(Handle, {
|
||||
type: 'source',
|
||||
position: Position.Bottom,
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
</el-button>
|
||||
<el-button @click="handleRun">运行</el-button>
|
||||
<el-button @click="handleClear">清空</el-button>
|
||||
<el-button type="warning" @click="handleTestAnimation" :loading="testingAnimation">
|
||||
<el-icon><VideoPlay /></el-icon>
|
||||
{{ testingAnimation ? '测试中...' : '测试动画' }}
|
||||
</el-button>
|
||||
<el-button v-if="copiedNode" @click="handlePasteNodeFromButton" type="success">
|
||||
<el-icon><DocumentCopy /></el-icon>
|
||||
粘贴节点 (Ctrl+V)
|
||||
@@ -1003,6 +1007,37 @@
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<!-- 结束节点配置 -->
|
||||
<template v-if="selectedNode.type === 'end' || selectedNode.type === 'output'">
|
||||
<el-form-item label="输出格式">
|
||||
<el-select v-model="selectedNode.data.output_format" placeholder="选择输出格式">
|
||||
<el-option label="纯文本(适合对话)" value="text" />
|
||||
<el-option label="JSON格式" value="json" />
|
||||
</el-select>
|
||||
<el-alert
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
style="margin-top: 5px;"
|
||||
>
|
||||
<template #title>
|
||||
<div style="font-size: 12px;">
|
||||
<strong>纯文本</strong>:自动提取文本内容,适合对话场景<br/>
|
||||
<strong>JSON格式</strong>:保留完整数据结构,适合API调用
|
||||
</div>
|
||||
</template>
|
||||
</el-alert>
|
||||
</el-form-item>
|
||||
<el-form-item label="描述">
|
||||
<el-input
|
||||
v-model="selectedNode.data.description"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="节点描述(可选)"
|
||||
/>
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<!-- 定时任务节点配置 -->
|
||||
<template v-if="isScheduleNodeSelected">
|
||||
<el-form-item label="延迟类型">
|
||||
@@ -1314,6 +1349,8 @@ const handleManageTemplates = () => {
|
||||
const executedNodeIds = ref<Set<string>>(new Set())
|
||||
const runningNodeId = ref<string | null>(null)
|
||||
const failedNodeIds = ref<Set<string>>(new Set())
|
||||
const testingAnimation = ref(false)
|
||||
const testAnimationTimer = ref<number | null>(null)
|
||||
|
||||
// 运行对话框
|
||||
const runDialogVisible = ref(false)
|
||||
@@ -1331,7 +1368,8 @@ const {
|
||||
addEdges,
|
||||
removeNodes,
|
||||
removeEdges,
|
||||
updateNode,
|
||||
updateNode,
|
||||
updateEdge,
|
||||
screenToFlowCoordinate,
|
||||
zoomIn: vueFlowZoomIn,
|
||||
zoomOut: vueFlowZoomOut,
|
||||
@@ -1558,6 +1596,10 @@ const handleDrop = (event: DragEvent) => {
|
||||
body_type: 'text',
|
||||
attachments: []
|
||||
} : {}),
|
||||
// 结束节点默认配置
|
||||
...(nodeType === 'end' || nodeType === 'output' ? {
|
||||
output_format: 'text' // 默认纯文本格式,适合对话场景
|
||||
} : {}),
|
||||
// 消息队列节点默认配置
|
||||
...(isMQNode ? {
|
||||
queue_type: 'rabbitmq',
|
||||
@@ -2385,6 +2427,211 @@ const handleClear = () => {
|
||||
ElMessage.success('画布已清空')
|
||||
}
|
||||
|
||||
// 测试动画 - 依次执行所有节点,展示完整的工作流动画效果
|
||||
const handleTestAnimation = () => {
|
||||
// 清除之前的测试状态
|
||||
if (testAnimationTimer.value) {
|
||||
clearTimeout(testAnimationTimer.value)
|
||||
testAnimationTimer.value = null
|
||||
}
|
||||
|
||||
// 找到第一个开始节点
|
||||
const startNode = nodes.value.find(n => n.type === 'start' || n.data?.type === 'start' || n.id.startsWith('start'))
|
||||
|
||||
if (!startNode) {
|
||||
ElMessage.warning('未找到开始节点,请先添加一个开始节点')
|
||||
return
|
||||
}
|
||||
|
||||
console.log('[rjb] 🧪 开始测试动画,节点ID:', startNode.id)
|
||||
|
||||
// 清除所有节点的执行状态
|
||||
nodes.value.forEach(node => {
|
||||
const nodeClass = (node.class || '').replace(/\b(executing|executed|failed)\b/g, '').trim()
|
||||
if (nodeClass !== node.class) {
|
||||
updateNode(node.id, {
|
||||
class: nodeClass,
|
||||
data: { ...node.data, executionStatus: undefined, executionClass: undefined }
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 清除所有边的执行状态
|
||||
edges.value.forEach(edge => {
|
||||
const edgeClass = (edge.class || '').replace(/\b(edge-executing|edge-executed)\b/g, '').trim()
|
||||
if (edgeClass !== edge.class) {
|
||||
if (updateEdge) {
|
||||
updateEdge(edge.id, {
|
||||
class: edgeClass,
|
||||
style: {
|
||||
...edge.style,
|
||||
stroke: '#409eff',
|
||||
strokeWidth: 2.5
|
||||
},
|
||||
animated: false
|
||||
})
|
||||
} else {
|
||||
edge.class = edgeClass
|
||||
edge.animated = false
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 使用广度优先搜索(BFS)找到所有节点的执行顺序
|
||||
const getExecutionOrder = (startNodeId: string): string[] => {
|
||||
const visited = new Set<string>()
|
||||
const order: string[] = []
|
||||
const queue: string[] = [startNodeId]
|
||||
|
||||
visited.add(startNodeId)
|
||||
|
||||
while (queue.length > 0) {
|
||||
const currentNodeId = queue.shift()!
|
||||
order.push(currentNodeId)
|
||||
|
||||
// 找到所有从当前节点出发的边
|
||||
const outgoingEdges = edges.value.filter(e => e.source === currentNodeId)
|
||||
for (const edge of outgoingEdges) {
|
||||
if (!visited.has(edge.target)) {
|
||||
visited.add(edge.target)
|
||||
queue.push(edge.target)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果还有未访问的节点(可能是孤立的),也加入顺序
|
||||
nodes.value.forEach(node => {
|
||||
if (!visited.has(node.id)) {
|
||||
order.push(node.id)
|
||||
}
|
||||
})
|
||||
|
||||
return order
|
||||
}
|
||||
|
||||
const executionOrder = getExecutionOrder(startNode.id)
|
||||
console.log('[rjb] 🧪 节点执行顺序:', executionOrder)
|
||||
|
||||
if (executionOrder.length === 0) {
|
||||
ElMessage.warning('未找到可执行的节点')
|
||||
return
|
||||
}
|
||||
|
||||
testingAnimation.value = true
|
||||
|
||||
// 依次执行每个节点
|
||||
const executeNodeAnimation = (nodeIndex: number) => {
|
||||
if (nodeIndex >= executionOrder.length) {
|
||||
// 所有节点执行完成
|
||||
testingAnimation.value = false
|
||||
testAnimationTimer.value = null
|
||||
ElMessage.success(`动画测试完成!共执行了 ${executionOrder.length} 个节点`)
|
||||
return
|
||||
}
|
||||
|
||||
const nodeId = executionOrder[nodeIndex]
|
||||
const node = nodes.value.find(n => n.id === nodeId)
|
||||
|
||||
if (!node) {
|
||||
// 如果节点不存在,跳过
|
||||
executeNodeAnimation(nodeIndex + 1)
|
||||
return
|
||||
}
|
||||
|
||||
console.log(`[rjb] 🧪 [${nodeIndex + 1}/${executionOrder.length}] 开始执行节点:`, nodeId)
|
||||
|
||||
// 第一步:标记为执行中
|
||||
const nodeClass = ((node.class || '').replace(/\b(executing|executed|failed)\b/g, '').trim() + ' executing').trim()
|
||||
const nodeData = { ...node.data, executionStatus: 'executing', executionClass: 'executing' }
|
||||
updateNode(node.id, {
|
||||
class: nodeClass,
|
||||
data: nodeData
|
||||
})
|
||||
console.log('[rjb] 🧪 ✅ 标记节点为执行中:', nodeId)
|
||||
|
||||
// 高亮连接到当前节点的边(从上游节点来的边)
|
||||
edges.value.forEach(edge => {
|
||||
if (edge.target === nodeId) {
|
||||
const edgeClass = ((edge.class || '').replace(/\b(edge-executing|edge-executed)\b/g, '').trim() + ' edge-executing').trim()
|
||||
try {
|
||||
if (updateEdge) {
|
||||
updateEdge(edge.id, {
|
||||
class: edgeClass,
|
||||
style: {
|
||||
...edge.style,
|
||||
stroke: '#409eff',
|
||||
strokeWidth: 3.5,
|
||||
strokeDasharray: '8,4'
|
||||
},
|
||||
animated: true
|
||||
})
|
||||
} else {
|
||||
edge.class = edgeClass
|
||||
edge.style = {
|
||||
...edge.style,
|
||||
stroke: '#409eff',
|
||||
strokeWidth: 3.5,
|
||||
strokeDasharray: '8,4'
|
||||
}
|
||||
edge.animated = true
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[rjb] Failed to update edge:', e)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 第二步:1.5秒后标记为已完成,然后执行下一个节点
|
||||
testAnimationTimer.value = window.setTimeout(() => {
|
||||
const executedNodeClass = ((node.class || '').replace(/\b(executing|executed|failed)\b/g, '').trim() + ' executed').trim()
|
||||
const executedNodeData = { ...node.data, executionStatus: 'executed', executionClass: 'executed' }
|
||||
updateNode(node.id, {
|
||||
class: executedNodeClass,
|
||||
data: executedNodeData
|
||||
})
|
||||
console.log('[rjb] 🧪 ✅ 标记节点为已完成:', nodeId)
|
||||
|
||||
// 更新连接到当前节点的边为已完成状态
|
||||
edges.value.forEach(edge => {
|
||||
if (edge.target === nodeId) {
|
||||
const edgeClass = ((edge.class || '').replace(/\b(edge-executing|edge-executed)\b/g, '').trim() + ' edge-executed').trim()
|
||||
try {
|
||||
if (updateEdge) {
|
||||
updateEdge(edge.id, {
|
||||
class: edgeClass,
|
||||
style: {
|
||||
...edge.style,
|
||||
stroke: '#67c23a',
|
||||
strokeWidth: 3,
|
||||
strokeDasharray: '0'
|
||||
},
|
||||
animated: false
|
||||
})
|
||||
} else {
|
||||
edge.class = edgeClass
|
||||
edge.style = {
|
||||
...edge.style,
|
||||
stroke: '#67c23a',
|
||||
strokeWidth: 3,
|
||||
strokeDasharray: '0'
|
||||
}
|
||||
edge.animated = false
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[rjb] Failed to update edge:', e)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 继续执行下一个节点
|
||||
executeNodeAnimation(nodeIndex + 1)
|
||||
}, 1500) // 每个节点执行1.5秒
|
||||
}
|
||||
|
||||
// 开始执行第一个节点
|
||||
executeNodeAnimation(0)
|
||||
}
|
||||
|
||||
// 监听节点和边的变化,检查是否有变更
|
||||
watch([nodes, edges], () => {
|
||||
if (lastSavedData.value) {
|
||||
@@ -2475,6 +2722,17 @@ watch(() => props.executionStatus, (newStatus, oldStatus) => {
|
||||
|
||||
const { current_node, executed_nodes, failed_nodes } = newStatus
|
||||
console.log('[rjb] Execution status - current:', current_node, 'executed:', executed_nodes, 'failed:', failed_nodes)
|
||||
console.log('[rjb] Execution status full:', JSON.stringify(newStatus, null, 2))
|
||||
|
||||
// 清除所有边的执行状态
|
||||
edges.value.forEach(edge => {
|
||||
if (edge.data) {
|
||||
delete edge.data.executionStatus
|
||||
}
|
||||
if (edge.class) {
|
||||
edge.class = edge.class.replace(/\b(edge-executing|edge-executed)\b/g, '').trim()
|
||||
}
|
||||
})
|
||||
|
||||
// 更新正在执行的节点
|
||||
if (current_node && current_node.node_id) {
|
||||
@@ -2486,7 +2744,49 @@ watch(() => props.executionStatus, (newStatus, oldStatus) => {
|
||||
class: nodeClass,
|
||||
data: nodeData
|
||||
})
|
||||
console.log('[rjb] ✅ Marking node as executing:', current_node.node_id, 'class:', nodeClass)
|
||||
console.log('[rjb] ✅ Marking node as executing:', current_node.node_id, 'class:', nodeClass, 'nodeData:', nodeData)
|
||||
|
||||
// 高亮连接到正在执行节点的边
|
||||
edges.value.forEach(edge => {
|
||||
if (edge.target === current_node.node_id) {
|
||||
const edgeClass = ((edge.class || '').replace(/\b(edge-executing|edge-executed)\b/g, '').trim() + ' edge-executing').trim()
|
||||
try {
|
||||
if (updateEdge) {
|
||||
updateEdge(edge.id, {
|
||||
class: edgeClass,
|
||||
style: {
|
||||
...edge.style,
|
||||
stroke: '#409eff',
|
||||
strokeWidth: 3.5,
|
||||
strokeDasharray: '8,4'
|
||||
},
|
||||
animated: true
|
||||
})
|
||||
} else {
|
||||
// 如果updateEdge不存在,直接修改(兼容旧版本)
|
||||
edge.class = edgeClass
|
||||
edge.style = {
|
||||
...edge.style,
|
||||
stroke: '#409eff',
|
||||
strokeWidth: 3.5,
|
||||
strokeDasharray: '8,4'
|
||||
}
|
||||
edge.animated = true
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[rjb] Failed to update edge:', e)
|
||||
// 直接修改作为后备方案
|
||||
edge.class = edgeClass
|
||||
edge.style = {
|
||||
...edge.style,
|
||||
stroke: '#409eff',
|
||||
strokeWidth: 3.5,
|
||||
strokeDasharray: '8,4'
|
||||
}
|
||||
edge.animated = true
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
console.warn('[rjb] ❌ Node not found:', current_node.node_id, 'available nodes:', nodes.value.map(n => n.id))
|
||||
}
|
||||
@@ -2505,6 +2805,36 @@ watch(() => props.executionStatus, (newStatus, oldStatus) => {
|
||||
data: nodeData
|
||||
})
|
||||
console.log('[rjb] ✅ Marking node as executed:', executedNode.node_id, 'class:', nodeClass)
|
||||
|
||||
// 高亮连接到已执行节点的边
|
||||
edges.value.forEach(edge => {
|
||||
if (edge.target === executedNode.node_id) {
|
||||
const edgeClass = ((edge.class || '').replace(/\b(edge-executing|edge-executed)\b/g, '').trim() + ' edge-executed').trim()
|
||||
try {
|
||||
updateEdge(edge.id, {
|
||||
class: edgeClass,
|
||||
style: {
|
||||
...edge.style,
|
||||
stroke: '#67c23a',
|
||||
strokeWidth: 3,
|
||||
strokeDasharray: '0'
|
||||
},
|
||||
animated: false
|
||||
})
|
||||
} catch (e) {
|
||||
console.warn('[rjb] Failed to update edge:', e)
|
||||
// 如果updateEdge不存在,直接修改(兼容旧版本)
|
||||
edge.class = edgeClass
|
||||
edge.style = {
|
||||
...edge.style,
|
||||
stroke: '#67c23a',
|
||||
strokeWidth: 3,
|
||||
strokeDasharray: '0'
|
||||
}
|
||||
edge.animated = false
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
console.warn('[rjb] ❌ Executed node not found:', executedNode.node_id)
|
||||
}
|
||||
@@ -2719,6 +3049,12 @@ onUnmounted(() => {
|
||||
// 禁用自动保存
|
||||
disableAutoSave()
|
||||
|
||||
// 清理测试动画定时器
|
||||
if (testAnimationTimer.value) {
|
||||
clearTimeout(testAnimationTimer.value)
|
||||
testAnimationTimer.value = null
|
||||
}
|
||||
|
||||
// 断开协作连接
|
||||
if (collaboration) {
|
||||
collaboration.disconnect()
|
||||
@@ -2987,19 +3323,26 @@ onUnmounted(() => {
|
||||
box-shadow: 0 0 0 3px rgba(64, 158, 255, 0.5), 0 0 20px rgba(64, 158, 255, 0.8) !important;
|
||||
animation: pulse-blue 1.5s infinite !important;
|
||||
transform: scale(1.05) !important;
|
||||
z-index: 10 !important;
|
||||
z-index: 3000 !important; /* Element Plus 模态框 z-index 通常是 2000-2100,我们设置更高 */
|
||||
position: relative !important;
|
||||
}
|
||||
|
||||
.custom-node.executed {
|
||||
.custom-node.executed,
|
||||
.vue-flow__node.executed .custom-node,
|
||||
.vue-flow__node .custom-node.executed {
|
||||
border: 3px solid #67c23a !important;
|
||||
box-shadow: 0 0 0 3px rgba(103, 194, 58, 0.5), 0 2px 8px rgba(103, 194, 58, 0.3) !important;
|
||||
z-index: 5 !important;
|
||||
z-index: 2999 !important; /* 比执行中的节点稍低,但仍在模态框之上 */
|
||||
position: relative !important;
|
||||
}
|
||||
|
||||
.custom-node.failed {
|
||||
.custom-node.failed,
|
||||
.vue-flow__node.failed .custom-node,
|
||||
.vue-flow__node .custom-node.failed {
|
||||
border: 3px solid #f56c6c !important;
|
||||
box-shadow: 0 0 0 3px rgba(245, 108, 108, 0.5), 0 2px 8px rgba(245, 108, 108, 0.3) !important;
|
||||
z-index: 5 !important;
|
||||
z-index: 2999 !important; /* 比执行中的节点稍低,但仍在模态框之上 */
|
||||
position: relative !important;
|
||||
}
|
||||
|
||||
/* 确保 Vue Flow 节点容器也能应用样式 */
|
||||
@@ -3013,18 +3356,24 @@ onUnmounted(() => {
|
||||
.vue-flow__node .custom-node.executing {
|
||||
border-color: #409eff !important;
|
||||
box-shadow: 0 0 0 3px rgba(64, 158, 255, 0.5), 0 0 20px rgba(64, 158, 255, 0.8) !important;
|
||||
animation: pulse-blue 1.5s infinite !important;
|
||||
animation: pulse-blue 1.5s infinite, node-pulse 2s ease-in-out infinite !important;
|
||||
transform: scale(1.05) !important;
|
||||
z-index: 3000 !important;
|
||||
position: relative !important;
|
||||
}
|
||||
|
||||
.vue-flow__node .custom-node.executed {
|
||||
border-color: #67c23a !important;
|
||||
box-shadow: 0 0 0 3px rgba(103, 194, 58, 0.5), 0 2px 8px rgba(103, 194, 58, 0.3) !important;
|
||||
z-index: 2999 !important;
|
||||
position: relative !important;
|
||||
}
|
||||
|
||||
.vue-flow__node .custom-node.failed {
|
||||
border-color: #f56c6c !important;
|
||||
box-shadow: 0 0 0 3px rgba(245, 108, 108, 0.5), 0 2px 8px rgba(245, 108, 108, 0.3) !important;
|
||||
z-index: 2999 !important;
|
||||
position: relative !important;
|
||||
}
|
||||
|
||||
@keyframes pulse-blue {
|
||||
@@ -3039,6 +3388,86 @@ onUnmounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
/* 旋转动画 - 用于加载指示器 */
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* 增强执行状态的动画效果 */
|
||||
.custom-node.executing {
|
||||
animation: pulse-blue 1.5s infinite, node-pulse 2s ease-in-out infinite !important;
|
||||
}
|
||||
|
||||
@keyframes node-pulse {
|
||||
0%, 100% {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.08);
|
||||
}
|
||||
}
|
||||
|
||||
/* 执行成功时的闪烁效果 */
|
||||
.custom-node.executed {
|
||||
animation: success-flash 0.5s ease-out !important;
|
||||
}
|
||||
|
||||
@keyframes success-flash {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgba(103, 194, 58, 0.5), 0 0 20px rgba(103, 194, 58, 0.8);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 0 8px rgba(103, 194, 58, 0.3), 0 0 30px rgba(103, 194, 58, 1);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 3px rgba(103, 194, 58, 0.5), 0 2px 8px rgba(103, 194, 58, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
/* 执行失败时的闪烁效果 */
|
||||
.custom-node.failed {
|
||||
animation: error-shake 0.5s ease-out !important;
|
||||
}
|
||||
|
||||
@keyframes error-shake {
|
||||
0%, 100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
10%, 30%, 50%, 70%, 90% {
|
||||
transform: translateX(-3px);
|
||||
}
|
||||
20%, 40%, 60%, 80% {
|
||||
transform: translateX(3px);
|
||||
}
|
||||
}
|
||||
|
||||
/* 边的执行状态样式 */
|
||||
.vue-flow__edge.edge-executing .vue-flow__edge-path {
|
||||
stroke: #409eff !important;
|
||||
stroke-width: 3.5 !important;
|
||||
stroke-dasharray: 8,4 !important;
|
||||
animation: edge-flow 1.5s linear infinite !important;
|
||||
}
|
||||
|
||||
.vue-flow__edge.edge-executed .vue-flow__edge-path {
|
||||
stroke: #67c23a !important;
|
||||
stroke-width: 3 !important;
|
||||
}
|
||||
|
||||
@keyframes edge-flow {
|
||||
0% {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
100% {
|
||||
stroke-dashoffset: 12;
|
||||
}
|
||||
}
|
||||
|
||||
/* 全局样式 - 优化边的选中效果(类似 Dify) */
|
||||
.vue-flow__edge.selected .vue-flow__edge-path {
|
||||
stroke: #67c23a !important;
|
||||
|
||||
Reference in New Issue
Block a user