- 提交后表单卡片默认收起(仅显示工作流标题),头部加明确展开/收起按钮 - node_start/node_complete 事件驱动节点步骤流,执行过程区仿豆包竖向展示 - 失败卡片拆分「重新执行」(复用已填值直接重跑)与「重新编辑」两按钮 - 失败消息只显示「❌ 执行失败」,错误详情由卡片 footer 展示,避免双处重复 Co-Authored-By: Claude <noreply@anthropic.com>
237 lines
7.0 KiB
Vue
237 lines
7.0 KiB
Vue
<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>
|