新增: Conditions 启用 + 工作流 DAG 展示 + 决策记录
- 开启 conditions-eval feature flag(df-workflow 默认启用) - 新增 WorkflowDagDisplay.vue 展示工作流节点/边/条件 - TaskDetail 工作流推进后自动获取并显示 DAG 结构 - 记录 2026-06-27 讨论决策到待决策.md - 记录 Inbox 设计想法到 todo.md
This commit is contained in:
131
src/components/workflow/WorkflowDagDisplay.vue
Normal file
131
src/components/workflow/WorkflowDagDisplay.vue
Normal file
@@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<div v-if="dag" class="wf-dag">
|
||||
<h4 class="wf-dag-title">{{ $t('taskDetail.workflowDagTitle') || '工作流结构' }}</h4>
|
||||
|
||||
<!-- 节点列表 -->
|
||||
<div class="wf-dag-nodes">
|
||||
<div v-for="node in dag.nodes" :key="node.id" class="wf-dag-node">
|
||||
<span class="wf-dag-node-icon">⬡</span>
|
||||
<span class="wf-dag-node-name">{{ node.id }}</span>
|
||||
<span class="wf-dag-node-type">{{ nodeTypeLabel[node.node_type] || node.node_type }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 边列表(带条件) -->
|
||||
<div class="wf-dag-edges">
|
||||
<div v-for="(edge, i) in dag.edges" :key="i" class="wf-dag-edge">
|
||||
<span class="wf-dag-edge-arrow">{{ edge.source }} → {{ edge.target }}</span>
|
||||
<span v-if="edge.condition" class="wf-dag-edge-cond" :title="edge.condition">
|
||||
{{ edge.condition }}
|
||||
</span>
|
||||
<span v-else class="wf-dag-edge-cond wf-dag-edge-cond--uncond">
|
||||
无条件
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="wf-dag wf-dag--empty">
|
||||
{{ $t('taskDetail.workflowDagEmpty') || '暂无工作流数据' }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
interface DagNode {
|
||||
id: string
|
||||
node_type: string
|
||||
config?: Record<string, unknown>
|
||||
}
|
||||
|
||||
interface EdgeDef {
|
||||
source: string
|
||||
target: string
|
||||
condition?: string | null
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
dagJson: string
|
||||
}>()
|
||||
|
||||
const dag = computed<{ nodes: DagNode[]; edges: EdgeDef[] } | null>(() => {
|
||||
try {
|
||||
return JSON.parse(props.dagJson)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
})
|
||||
|
||||
const nodeTypeLabel: Record<string, string> = {
|
||||
'AiNode': 'AI',
|
||||
'HumanNode': '人工审批',
|
||||
'ScriptNode': '脚本',
|
||||
'AiSelfReviewNode': 'AI 自审',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.wf-dag {
|
||||
margin-top: 8px;
|
||||
padding: 8px 12px;
|
||||
background: var(--df-bg-card, #fafafa);
|
||||
border-radius: var(--df-radius-sm, 4px);
|
||||
font-size: 12px;
|
||||
}
|
||||
.wf-dag--empty {
|
||||
color: var(--df-text-dim, #888);
|
||||
}
|
||||
.wf-dag-title {
|
||||
margin: 0 0 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.wf-dag-nodes {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.wf-dag-node {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 3px 8px;
|
||||
background: var(--df-bg, #fff);
|
||||
border: 0.5px solid var(--df-border, #ddd);
|
||||
border-radius: var(--df-radius-sm, 4px);
|
||||
}
|
||||
.wf-dag-node-icon { color: var(--df-text-dim, #888); }
|
||||
.wf-dag-node-name { font-weight: 500; }
|
||||
.wf-dag-node-type {
|
||||
font-size: 10px;
|
||||
color: var(--df-text-dim, #888);
|
||||
background: var(--df-bg-card, #f0f0f0);
|
||||
padding: 0 4px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.wf-dag-edges {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
.wf-dag-edge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 2px 0;
|
||||
}
|
||||
.wf-dag-edge-arrow { color: var(--df-text-primary); }
|
||||
.wf-dag-edge-cond {
|
||||
font-family: var(--df-font-mono);
|
||||
font-size: 11px;
|
||||
padding: 1px 6px;
|
||||
background: rgba(74, 144, 226, 0.08);
|
||||
border-radius: 3px;
|
||||
color: var(--df-info, #4a90e2);
|
||||
}
|
||||
.wf-dag-edge-cond--uncond {
|
||||
background: transparent;
|
||||
color: var(--df-text-dim, #aaa);
|
||||
}
|
||||
</style>
|
||||
@@ -168,6 +168,7 @@ import {
|
||||
} from '../constants/project'
|
||||
import type { TaskRecord, ProjectRecord, IdeaRecord, DfDataChangedPayload, WorkflowEventPayload } from '@/api/types'
|
||||
import TaskOutputCard from '@/components/task/TaskOutputCard.vue'
|
||||
import WorkflowDagDisplay from '@/components/workflow/WorkflowDagDisplay.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
@@ -198,6 +199,15 @@ let _wfResultTimer: ReturnType<typeof setTimeout> | null = null
|
||||
const wfProgressHint = computed(() => wfResult.value !== null)
|
||||
const wfCompletedHint = computed(() => wfResult.value === 'completed')
|
||||
const wfFailedHint = computed(() => wfResult.value === 'failed')
|
||||
|
||||
// 工作流 DAG 结构展示
|
||||
const wfDagJson = ref('')
|
||||
async function refreshWorkflowDag(execId: string) {
|
||||
try {
|
||||
const record = await workflowApi.getExecution(execId)
|
||||
if (record?.dag_json) wfDagJson.value = record.dag_json
|
||||
} catch { /* 静默 */ }
|
||||
}
|
||||
const wfDoneTotal = computed(() => wfTotalNodes.value)
|
||||
|
||||
const taskId = computed(() => route.params.id as string)
|
||||
@@ -337,6 +347,7 @@ async function handleWorkflowAdvance(target: string) {
|
||||
)
|
||||
wfExecId.value = execId
|
||||
errorMsg.value = ''
|
||||
await refreshWorkflowDag(execId)
|
||||
} catch (e: any) {
|
||||
wfAdvancing.value = false
|
||||
wfExecId.value = null
|
||||
|
||||
Reference in New Issue
Block a user