首页工作流执行:表单默认收起可展开、节点执行过程可视化(仿豆包)、失败支持重新执行/重新编辑、错误信息去重

- 提交后表单卡片默认收起(仅显示工作流标题),头部加明确展开/收起按钮
- node_start/node_complete 事件驱动节点步骤流,执行过程区仿豆包竖向展示
- 失败卡片拆分「重新执行」(复用已填值直接重跑)与「重新编辑」两按钮
- 失败消息只显示「 执行失败」,错误详情由卡片 footer 展示,避免双处重复

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-21 17:07:40 +08:00
co-authored by Claude
parent a95fe2d73f
commit cb6792cb29
5 changed files with 298 additions and 33 deletions
+194 -19
View File
@@ -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;