Files
admin-ui/src/views/home/components/MainContent.vue
T
2910410219andClaude cb6792cb29 首页工作流执行:表单默认收起可展开、节点执行过程可视化(仿豆包)、失败支持重新执行/重新编辑、错误信息去重
- 提交后表单卡片默认收起(仅显示工作流标题),头部加明确展开/收起按钮
- node_start/node_complete 事件驱动节点步骤流,执行过程区仿豆包竖向展示
- 失败卡片拆分「重新执行」(复用已填值直接重跑)与「重新编辑」两按钮
- 失败消息只显示「 执行失败」,错误详情由卡片 footer 展示,避免双处重复

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-21 17:07:40 +08:00

237 lines
7.0 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="main-content">
<Transition name="content-fade" mode="out-in">
<!-- 对话页 有消息或会话结果时展示消息流工作流表单卡片/结果/产出统一在此)。
key 绑定会话 id切换会话时重建内容区触发 Transition 过渡动画固定 key 会复用 DOM 不触发 -->
<div v-if="hasMessages || hasResults" :key="activeHistoryId || 'chat'" class="content-body chat-body">
<ChatList
:messages="messages"
:has-more="hasMore"
:loading-more="loadingMore"
class="chat-list-scroll"
@retry="emit('retry', $event)"
@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)"
@output-download="(msg: any, output: any) => emit('output-download', msg, output)"
@output-delete="(msg: any, output: any) => emit('output-delete', msg, output)"
@load-more="emit('load-more', activeHistoryId)"
/>
</div>
<!-- 默认占位 — 无工作流时显示引导 -->
<div v-else :key="'placeholder'" class="content-body placeholder-body">
<div class="placeholder-content">
<h2 class="placeholder-title">你好,我能帮你解决什么问题?</h2>
<p class="placeholder-desc">选择一个工作流,填写参数后一键生成</p>
<div v-if="placeWorkflows.length" class="workflow-cards">
<button
v-for="wf in placeWorkflows"
:key="wf.id"
type="button"
class="workflow-card"
@click="handlePlaceSelect(wf)"
>
<el-icon class="wf-card-icon"><Promotion /></el-icon>
<span class="wf-card-name">{{ wf.name }}</span>
<span v-if="wf.isTemplate" class="wf-card-tag">模板</span>
</button>
</div>
<div v-else class="placeholder-empty">暂无工作流,可在工作流管理中创建</div>
</div>
</div>
</Transition>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue';
import { getWorkflowList } from '/@/api/settings/creation';
import { Promotion } from '@element-plus/icons-vue';
import ChatList from './ChatList.vue';
import type { VOSessionInfoResult } from '/@/api/settings/workflow/session';
interface Props {
activeMenu: string;
activeHistoryId?: string | null;
messages?: any[];
// 会话内结果(session/get):仅用于判断「对话页 vs 占位页」,结果渲染统一走消息流
results?: VOSessionInfoResult[];
hasMore?: boolean;
loadingMore?: boolean;
}
interface Emits {
(e: 'retry', msg: any): void;
(e: 'regenerate', msg: any): void;
(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;
(e: 'output-download', msg: any, output: any): void;
(e: 'output-delete', msg: any, output: any): void;
(e: 'load-more', sid: string | null | undefined): void;
}
const props = withDefaults(defineProps<Props>(), {
activeHistoryId: null,
messages: () => [],
results: () => [],
hasMore: false,
loadingMore: false,
});
const emit = defineEmits<Emits>();
const hasMessages = computed(() => Array.isArray(props.messages) && props.messages.length > 0);
const hasResults = computed(() => Array.isArray(props.results) && props.results.length > 0);
// 占位页工作流卡片(DeepSeek 式引导:居中标题 + 工作流卡片)
const placeWorkflows = ref<any[]>([]);
const loadPlaceWorkflows = async () => {
try {
const res: any = await getWorkflowList();
const userList = res.data?.listFlowUserRes?.list || [];
const tplList = res.data?.listFlowTemplateRes?.list || [];
placeWorkflows.value = [
...userList.map((w: any) => ({ id: String(w.id), name: w.flowName || '未命名', isTemplate: false })),
...tplList.map((w: any) => ({ id: String(w.id), name: w.flowTemplateName || '未命名', isTemplate: true })),
];
} catch {
placeWorkflows.value = [];
}
};
const handlePlaceSelect = (wf: any) => {
emit('workflow-select', wf.id, wf.isTemplate);
};
onMounted(() => {
loadPlaceWorkflows();
});
</script>
<style scoped lang="scss">
/* ===== 页面切换过渡:淡入淡出 + 轻微上移 ===== */
.content-fade-enter-active,
.content-fade-leave-active {
transition: opacity 0.25s ease, transform 0.25s ease;
}
.content-fade-enter-from {
opacity: 0;
transform: translateY(8px);
}
.content-fade-leave-to {
opacity: 0;
transform: translateY(-8px);
}
.main-content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.content-body {
flex: 1;
overflow-y: auto;
scrollbar-width: none;
-ms-overflow-style: none;
&::-webkit-scrollbar { display: none; }
}
/* ===== 对话页 ===== */
.chat-body {
width: min(880px, calc(100% - 40px));
margin: 0 auto;
height: 100%;
display: flex;
flex-direction: column;
padding: 0;
box-sizing: border-box;
overflow: hidden; /* 结果列表固定高度、消息流内部滚动,避免整体滚动与内部冲突 */
scrollbar-width: none;
&::-webkit-scrollbar { display: none; }
}
/* 消息流填充剩余高度并在内部滚动 */
.chat-body :deep(.chat-list-scroll) {
flex: 1;
min-height: 0;
height: auto;
}
.placeholder-body { display: flex; align-items: center; justify-content: center; }
.placeholder-content { text-align: center; padding: 24px 20px; width: 100%; max-width: 740px; }
/* ===== 标题与描述 ===== */
.placeholder-title { font-size: 26px; font-weight: 700; color: #0f172a; margin: 0 0 8px; letter-spacing: -0.2px; line-height: 1.4; }
.placeholder-desc { font-size: 15px; color: #94a3b8; margin: 0 0 24px; line-height: 1.5; }
/* ===== 工作流功能卡片(DeepSeek 式引导) ===== */
.workflow-cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 12px;
text-align: left;
}
.workflow-card {
display: flex;
align-items: center;
gap: 10px;
padding: 18px 20px;
border: 1px solid #e5e8ee;
border-radius: 14px;
background: #fff;
cursor: pointer;
transition: all 0.15s;
outline: none;
&:hover {
border-color: #93c5fd;
box-shadow: 0 4px 16px rgba(59, 130, 246, 0.12);
transform: translateY(-1px);
}
}
.wf-card-icon {
width: 40px;
height: 40px;
border-radius: 12px;
background: #eaf1ff;
color: #2563eb;
font-size: 20px;
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.wf-card-name {
flex: 1;
min-width: 0;
font-size: 15px;
font-weight: 600;
color: #1e293b;
line-height: 1.4;
word-break: break-word;
}
.wf-card-tag {
font-size: 10px;
padding: 2px 6px;
border-radius: 4px;
background: #fff7e6;
color: #d97706;
border: 1px solid #fde68a;
white-space: nowrap;
}
.placeholder-empty {
font-size: 13px;
color: #cbd5e1;
padding: 16px 0;
}
</style>