首页工作流会话重构与产出内嵌:后端 UUID 认领 + 产出对话内直接预览

会话机制:
- 建连不传本地会话号,由后端生成 UUID 并经首帧 ack 认领(claimSessionId 就地迁移全部状态)
- 删除执行完成后轮询会话列表替换本地条目的双路径逻辑,统一认领后刷新
- 清理 virtual_ 特判,收敛为 hasBackendSessionId 单一语义判断

产出展示:
- flow_complete 事件自带 resultFileUrls 直接构建产出卡片(buildOutputsFromUrls)
- 完成文案改为「 执行完成」,产出以消息形式展示在对话里
- 产出媒体(图片/视频/音频)常驻内嵌预览,图片点击放大,text/file 保持文件行
- ChatList 自动续载增加次数上限,防止 hasMore 恒真时无限加载循环
This commit is contained in:
2026-08-17 17:59:24 +08:00
parent bb15c053e6
commit 13a2aeee9f
6 changed files with 349 additions and 110 deletions
+139 -24
View File
@@ -1,22 +1,62 @@
<template>
<div class="workflow-output-card">
<div v-for="(out, i) in outputs" :key="i" class="output-item">
<el-icon class="output-icon" :class="`is-${out.type || 'file'}`">
<component :is="getIcon(out.type)" />
</el-icon>
<span class="output-name" :title="out.name">{{ out.name }}</span>
<span class="output-actions">
<button v-if="out.type !== 'text'" type="button" class="output-action" title="预览" @click="emit('preview', out)">
<el-icon><View /></el-icon>
</button>
<button type="button" class="output-action" title="下载" @click="emit('download', out)">
<el-icon><Download /></el-icon>
</button>
<button type="button" class="output-action is-danger" title="删除" @click="emit('delete', out)">
<el-icon><Delete /></el-icon>
</button>
</span>
</div>
<template v-for="(out, i) in outputs" :key="i">
<!-- 图片常驻内嵌缩略图点击放大看大图走父级预览弹窗 -->
<div v-if="out.type === 'image'" class="output-media is-image" @click="emit('preview', out)">
<img :src="resolveUrl ? resolveUrl(out.url) : out.url" :alt="out.name" loading="lazy" />
<span class="media-name">{{ out.name }}</span>
<span class="media-actions" @click.stop>
<button type="button" class="output-action" title="下载" @click="emit('download', out)">
<el-icon><Download /></el-icon>
</button>
<button type="button" class="output-action is-danger" title="删除" @click="emit('delete', out)">
<el-icon><Delete /></el-icon>
</button>
</span>
</div>
<!-- 视频内嵌播放器底部不叠加文件名避免遮挡原生控制条 -->
<div v-else-if="out.type === 'video'" class="output-media is-video">
<video :src="resolveUrl ? resolveUrl(out.url) : out.url" controls preload="metadata" :title="out.name"></video>
<span class="media-actions" @click.stop>
<button type="button" class="output-action" title="下载" @click="emit('download', out)">
<el-icon><Download /></el-icon>
</button>
<button type="button" class="output-action is-danger" title="删除" @click="emit('delete', out)">
<el-icon><Delete /></el-icon>
</button>
</span>
</div>
<!-- 音频内嵌播放器底部不叠加文件名避免遮挡原生控制条 -->
<div v-else-if="out.type === 'audio'" class="output-media is-audio">
<audio :src="resolveUrl ? resolveUrl(out.url) : out.url" controls preload="metadata" :title="out.name"></audio>
<span class="media-actions" @click.stop>
<button type="button" class="output-action" title="下载" @click="emit('download', out)">
<el-icon><Download /></el-icon>
</button>
<button type="button" class="output-action is-danger" title="删除" @click="emit('delete', out)">
<el-icon><Delete /></el-icon>
</button>
</span>
</div>
<!-- 文本 / 其他类型文件行 + 预览/下载/删除 -->
<div v-else class="output-item">
<el-icon class="output-icon" :class="`is-${out.type || 'file'}`">
<component :is="getIcon(out.type)" />
</el-icon>
<span class="output-name" :title="out.name">{{ out.name }}</span>
<span class="output-actions">
<button v-if="out.type !== 'text'" type="button" class="output-action" title="预览" @click="emit('preview', out)">
<el-icon><View /></el-icon>
</button>
<button type="button" class="output-action" title="下载" @click="emit('download', out)">
<el-icon><Download /></el-icon>
</button>
<button type="button" class="output-action is-danger" title="删除" @click="emit('delete', out)">
<el-icon><Delete /></el-icon>
</button>
</span>
</div>
</template>
</div>
</template>
@@ -33,6 +73,8 @@ interface WorkflowOutput {
interface Props {
outputs: WorkflowOutput[];
// 相对路径 → 完整 URL 解析函数(由父级传入 buildAssetUrl),缺省时直接用原 url(绝对地址场景)
resolveUrl?: (url: string) => string;
}
interface Emits {
@@ -58,20 +100,76 @@ const getIcon = (type: string) => {
.workflow-output-card {
display: flex;
flex-direction: column;
gap: 6px;
gap: 8px;
margin-top: 10px;
background: #f8fafc;
border: 1px solid #f1f5f9;
border-radius: 10px;
padding: 8px;
}
// 媒体产出(图片/视频/音频):常驻内嵌展示,hover 浮出下载/删除操作
.output-media {
position: relative;
overflow: hidden;
border-radius: 10px;
background: #0f172a;
img,
video {
display: block;
width: 100%;
max-height: 320px;
object-fit: contain;
background: #0f172a;
}
audio {
display: block;
width: 100%;
background: #0f172a;
padding: 10px;
}
.media-name {
position: absolute;
left: 0;
right: 0;
bottom: 0;
padding: 4px 8px;
font-size: 12px;
color: #e2e8f0;
background: linear-gradient(transparent, rgba(0, 0, 0, 0.6));
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.media-actions {
position: absolute;
top: 6px;
right: 6px;
display: flex;
gap: 2px;
padding: 2px;
border-radius: 8px;
background: rgba(15, 23, 42, 0.75);
opacity: 0;
transition: opacity 0.15s;
}
&:hover {
.media-actions {
opacity: 1;
}
}
}
// 非媒体产出:文件行 + 操作按钮
.output-item {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 8px;
border-radius: 8px;
padding: 8px 10px;
background: #f8fafc;
border: 1px solid #f1f5f9;
border-radius: 10px;
transition: background 0.15s;
&:hover {
@@ -145,4 +243,21 @@ const getIcon = (type: string) => {
font-size: 13px;
}
}
// 媒体产出内的操作按钮:深色背景上使用浅色图标
.output-media {
.output-action {
color: #cbd5e1;
&:hover {
background: rgba(255, 255, 255, 0.18);
color: #f8fafc;
}
&.is-danger:hover {
background: rgba(220, 38, 38, 0.35);
color: #fecaca;
}
}
}
</style>