首页工作流执行:表单默认收起可展开、节点执行过程可视化(仿豆包)、失败支持重新执行/重新编辑、错误信息去重
- 提交后表单卡片默认收起(仅显示工作流标题),头部加明确展开/收起按钮 - node_start/node_complete 事件驱动节点步骤流,执行过程区仿豆包竖向展示 - 失败卡片拆分「重新执行」(复用已填值直接重跑)与「重新编辑」两按钮 - 失败消息只显示「❌ 执行失败」,错误详情由卡片 footer 展示,避免双处重复 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,7 @@
|
||||
:form-error="msg.formError"
|
||||
:progress="msg.formProgress"
|
||||
@submit="emit('form-submit', msg, $event)"
|
||||
@retry="emit('form-retry', msg, $event)"
|
||||
@re-edit="emit('form-re-edit', msg)"
|
||||
@cancel="emit('form-cancel', msg)"
|
||||
/>
|
||||
@@ -100,6 +101,7 @@ import { Delete, DocumentCopy, RefreshRight } from '@element-plus/icons-vue';
|
||||
import 'highlight.js/styles/github-dark.css';
|
||||
import { renderMarkdown } from '../utils/markdown';
|
||||
import type { WorkflowOutput } from '../utils/flowDsl';
|
||||
import type { WorkflowNodeProgress } from '../utils/wsMessage';
|
||||
import WorkflowFormCard from './WorkflowFormCard.vue';
|
||||
import WorkflowOutputCard from './WorkflowOutputCard.vue';
|
||||
|
||||
@@ -120,8 +122,8 @@ interface ChatMessage {
|
||||
form?: any;
|
||||
formStatus?: 'editing' | 'running' | 'done' | 'failed';
|
||||
formError?: string;
|
||||
// 工作流执行节点进度(后端 node_start/node_complete 事件推进,运行中显示)
|
||||
formProgress?: { current: number; total: number; nodeName: string };
|
||||
// 工作流执行节点进度(node_start/node_complete 事件驱动的节点步骤列表,渲染执行过程区)
|
||||
formProgress?: WorkflowNodeProgress;
|
||||
// 工作流执行产出文件列表(挂在 workflow 结果消息上,渲染产出卡片)
|
||||
outputs?: WorkflowOutput[];
|
||||
}
|
||||
@@ -131,6 +133,7 @@ interface Emits {
|
||||
(e: 'regenerate', msg: ChatMessage): void;
|
||||
(e: 'delete', msg: ChatMessage): void;
|
||||
(e: 'form-submit', msg: ChatMessage, payload: any): void;
|
||||
(e: 'form-retry', msg: ChatMessage, payload: any): void;
|
||||
(e: 'form-re-edit', msg: ChatMessage): void;
|
||||
(e: 'form-cancel', msg: ChatMessage): void;
|
||||
(e: 'output-preview', msg: ChatMessage, output: WorkflowOutput): void;
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
@regenerate="emit('regenerate', $event)"
|
||||
@delete="emit('delete', $event)"
|
||||
@form-submit="(msg: any, payload: any) => emit('form-submit', msg, payload)"
|
||||
@form-retry="(msg: any, payload: any) => emit('form-retry', msg, payload)"
|
||||
@form-re-edit="(msg: any) => emit('form-re-edit', msg)"
|
||||
@form-cancel="(msg: any) => emit('form-cancel', msg)"
|
||||
@output-preview="(msg: any, output: any) => emit('output-preview', msg, output)"
|
||||
@@ -71,6 +72,7 @@ interface Emits {
|
||||
(e: 'workflow-select', id: string, isTemplate?: boolean): void;
|
||||
(e: 'delete', msg: any): void;
|
||||
(e: 'form-submit', msg: any, payload: any): void;
|
||||
(e: 'form-retry', msg: any, payload: any): void;
|
||||
(e: 'form-re-edit', msg: any): void;
|
||||
(e: 'form-cancel', msg: any): void;
|
||||
(e: 'output-preview', msg: any, output: any): void;
|
||||
|
||||
@@ -1,12 +1,37 @@
|
||||
<template>
|
||||
<div class="workflow-form-card" :class="{ readonly, submitting, failed: isFailed }">
|
||||
<div class="workflow-form-header">
|
||||
<div>
|
||||
<div class="workflow-form-header" @click="toggleCollapse">
|
||||
<div class="header-left">
|
||||
<h3>{{ detail?.flowName || '工作流表单' }}</h3>
|
||||
<div class="workflow-form-sub">{{ detail?.description || '请填写以下参数' }}</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<el-tag v-if="submitting" size="small" type="primary">执行中</el-tag>
|
||||
<el-tag v-else-if="isFailed" size="small" type="danger">执行失败</el-tag>
|
||||
<el-tag v-else-if="readonly" size="small" type="success">执行完成</el-tag>
|
||||
<!-- 折叠切换:明确按钮提示用户可展开/收起已填参数(点击标题行同样可切换) -->
|
||||
<button type="button" class="collapse-toggle" @click.stop="toggleCollapse">
|
||||
<el-icon class="collapse-arrow" :class="{ collapsed }"><ArrowDown /></el-icon>
|
||||
<span>{{ collapsed ? '展开' : '收起' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="workflow-form-scroll">
|
||||
|
||||
<!-- 节点执行过程区(仿豆包步骤流):有节点事件才显示,始终可见、不受参数收起影响 -->
|
||||
<div v-if="progress?.nodes?.length" class="node-progress-list">
|
||||
<div v-for="node in progress.nodes" :key="node.nodeId" class="node-step" :class="'is-' + node.status">
|
||||
<span class="node-step-icon">
|
||||
<span v-if="node.status === 'running'" class="node-spinner" />
|
||||
<el-icon v-else-if="node.status === 'done'" class="node-icon-done"><CircleCheckFilled /></el-icon>
|
||||
<el-icon v-else-if="node.status === 'failed'" class="node-icon-failed"><CircleCloseFilled /></el-icon>
|
||||
<span v-else class="node-dot" />
|
||||
</span>
|
||||
<span class="node-step-name">{{ node.nodeName }}</span>
|
||||
<span class="node-step-status">{{ nodeStatusText(node) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-show="!collapsed" class="workflow-form-scroll">
|
||||
<el-form label-position="top" class="workflow-form">
|
||||
<template v-if="detail?.nodeInputParams">
|
||||
<template v-for="node in detail.nodeInputParams" :key="node.id || node.nodeCode">
|
||||
@@ -153,10 +178,13 @@
|
||||
<el-tag :type="isFailed ? 'danger' : 'success'" size="small" effect="light">
|
||||
{{ isFailed ? '执行失败' : '执行完成' }}
|
||||
</el-tag>
|
||||
<!-- 失败卡片可「重新编辑并执行」:保留已填值切回可编辑,改完重新提交 -->
|
||||
<el-button v-if="isFailed" size="small" @click="emit('re-edit')">
|
||||
<el-icon><RefreshRight /></el-icon>重新编辑并执行
|
||||
</el-button>
|
||||
<!-- 失败卡片:可直接「重新执行」(复用当前已填值重跑),或「重新编辑」修改参数后再提交 -->
|
||||
<template v-if="isFailed">
|
||||
<el-button size="small" type="primary" @click="handleRetry">
|
||||
<el-icon><RefreshRight /></el-icon>重新执行
|
||||
</el-button>
|
||||
<el-button size="small" @click="emit('re-edit')">重新编辑</el-button>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
@@ -165,10 +193,11 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref, watch } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { RefreshRight } from '@element-plus/icons-vue';
|
||||
import { ArrowDown, CircleCheckFilled, CircleCloseFilled, RefreshRight } from '@element-plus/icons-vue';
|
||||
import PatchTemplateEditor from '/@/components/patchTemplate/PatchTemplateEditor.vue';
|
||||
import { uploadFile } from '/@/api/common/upload';
|
||||
import { collectHomeFormFields } from '../utils/flowDsl';
|
||||
import type { WorkflowNodeStep } from '../utils/wsMessage';
|
||||
|
||||
interface Props {
|
||||
// 工作流详情(含 nodeInputParams / flowContent)
|
||||
@@ -179,14 +208,16 @@ interface Props {
|
||||
submitting?: boolean;
|
||||
// 执行失败信息(有值即 failed 态)
|
||||
formError?: string;
|
||||
// 执行进度(后端 node_start/node_complete 事件推进):当前节点序 / 总节点数 / 节点名
|
||||
progress?: { current: number; total: number; nodeName: string };
|
||||
// 执行节点进度(node_start/node_complete 事件驱动的步骤列表,渲染执行过程区)
|
||||
progress?: { nodes: WorkflowNodeStep[] };
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'submit', payload: { formValues: Record<string, any>; formFileNames: Record<string, string | string[]>; templates: any[] }): void;
|
||||
// 失败卡片「重新编辑并执行」:由父组件把 formStatus 切回 editing,保留已填值
|
||||
(e: 're-edit'): void;
|
||||
// 失败卡片「重新执行」:复用当前已填值直接重跑(不进入编辑态)
|
||||
(e: 'retry', payload: { formValues: Record<string, any>; formFileNames: Record<string, string | string[]>; templates: any[] }): void;
|
||||
// 执行中取消:工作流取消入口(InputBar 停止按钮对工作流禁用)
|
||||
(e: 'cancel'): void;
|
||||
}
|
||||
@@ -195,7 +226,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
readonly: false,
|
||||
submitting: false,
|
||||
formError: '',
|
||||
progress: () => ({ current: 0, total: 0, nodeName: '' }),
|
||||
progress: () => ({ nodes: [] }),
|
||||
});
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
@@ -210,15 +241,52 @@ const templates = ref<any[]>([]);
|
||||
const isFailed = computed(() => !!props.formError);
|
||||
const isDisabled = computed(() => props.readonly || props.submitting);
|
||||
|
||||
// 执行进度文本:有节点进度(current/total>0)显示「正在执行 N/M:节点名」,否则兜底「执行中...」
|
||||
const progressText = computed(() => {
|
||||
const p = props.progress;
|
||||
if (p && p.total > 0 && p.current > 0) {
|
||||
const name = p.nodeName ? `:${p.nodeName}` : '';
|
||||
return `正在执行 ${p.current}/${p.total}${name}...`;
|
||||
}
|
||||
return '执行中...';
|
||||
// 表单收起/展开:editing(待提交/回显)默认展开;提交(running)自动收起;
|
||||
// done/failed 保持提交时状态(默认收起,用户可手动展开查看已填值)
|
||||
const collapsed = ref(false);
|
||||
const formStatus = computed(() => {
|
||||
if (props.submitting) return 'running';
|
||||
if (props.formError) return 'failed';
|
||||
if (props.readonly) return 'done';
|
||||
return 'editing';
|
||||
});
|
||||
watch(
|
||||
formStatus,
|
||||
(st, prev) => {
|
||||
if (st === 'editing') {
|
||||
collapsed.value = false; // 回显/重新编辑:默认展开
|
||||
} else if (prev === undefined) {
|
||||
collapsed.value = true; // 初始即非编辑态(执行中/完成/失败)→ 收起
|
||||
} else if (st === 'running') {
|
||||
collapsed.value = true; // 提交瞬间自动收起
|
||||
}
|
||||
// done/failed 到达时保持当前(提交时已收起,用户手动展开则保留)
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
const toggleCollapse = () => {
|
||||
collapsed.value = !collapsed.value;
|
||||
};
|
||||
|
||||
// 执行进度文本:有进行中节点显示「正在执行:节点名」,否则兜底「执行中...」
|
||||
const progressText = computed(() => {
|
||||
const running = (props.progress?.nodes || []).find((n) => n.status === 'running');
|
||||
return running ? `正在执行:${running.nodeName}` : '执行中...';
|
||||
});
|
||||
|
||||
// 节点步骤状态文案(执行过程区)
|
||||
const nodeStatusText = (node: WorkflowNodeStep): string => {
|
||||
switch (node.status) {
|
||||
case 'running':
|
||||
return '执行中';
|
||||
case 'done':
|
||||
return '完成';
|
||||
case 'failed':
|
||||
return '失败';
|
||||
default:
|
||||
return '待执行';
|
||||
}
|
||||
};
|
||||
|
||||
const getFieldKey = (node: any, field: any): string => {
|
||||
const id = node.id || node.nodeCode;
|
||||
@@ -481,6 +549,16 @@ const handleSubmit = () => {
|
||||
templates: JSON.parse(JSON.stringify(templates.value || [])),
|
||||
});
|
||||
};
|
||||
|
||||
// 失败后直接重跑:复用当前已填值(必填此前已校验通过),不进入编辑态原样重新提交
|
||||
const handleRetry = () => {
|
||||
if (!isFailed.value || props.submitting) return;
|
||||
emit('retry', {
|
||||
formValues: JSON.parse(JSON.stringify(formValues)),
|
||||
formFileNames: JSON.parse(JSON.stringify(formFileNames)),
|
||||
templates: JSON.parse(JSON.stringify(templates.value || [])),
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -512,9 +590,106 @@ const handleSubmit = () => {
|
||||
padding: 12px 20px;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: background 0.15s;
|
||||
&:hover { background: #f8fafc; }
|
||||
h3 { margin: 0; font-size: 14px; font-weight: 600; color: #0f172a; }
|
||||
.workflow-form-sub { font-size: 12px; color: #94a3b8; margin-top: 2px; }
|
||||
}
|
||||
.header-left { min-width: 0; }
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
/* 折叠切换按钮:文字 + 箭头,明确提示可展开/收起参数区 */
|
||||
.collapse-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
padding: 3px 9px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
&:hover {
|
||||
border-color: #93c5fd;
|
||||
color: #2563eb;
|
||||
background: #eff6ff;
|
||||
}
|
||||
.collapse-arrow {
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
transition: transform 0.2s;
|
||||
&.collapsed { transform: rotate(-180deg); }
|
||||
}
|
||||
}
|
||||
|
||||
/* 节点执行过程区(豆包式步骤流) */
|
||||
.node-progress-list {
|
||||
background: #f8fafc;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
padding: 10px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.node-step {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 13px;
|
||||
|
||||
.node-step-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.node-step-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: #1e293b;
|
||||
}
|
||||
.node-step-status {
|
||||
flex-shrink: 0;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
}
|
||||
&.is-running .node-step-status { color: #2563eb; }
|
||||
&.is-done .node-step-status { color: #22c55e; }
|
||||
&.is-failed .node-step-status { color: #ef4444; }
|
||||
}
|
||||
.node-spinner {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border: 2px solid #dbeafe;
|
||||
border-top-color: #2563eb;
|
||||
border-radius: 50%;
|
||||
animation: node-spin 0.8s linear infinite;
|
||||
}
|
||||
.node-icon-done { color: #22c55e; font-size: 15px; }
|
||||
.node-icon-failed { color: #ef4444; font-size: 15px; }
|
||||
.node-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: #cbd5e1;
|
||||
}
|
||||
@keyframes node-spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.workflow-form-scroll {
|
||||
max-height: 55vh;
|
||||
|
||||
Reference in New Issue
Block a user