首页工作空间下载:对象名传参适配后端相对路径、下载感知(toast+按钮级)、5分钟超时

- downloadAsset 重写:content 相对路径直接作下载参数,兼容历史完整 URL 去域名;
  blob 校验拒绝 JSON 错误体,避免生成坏文件;文件名带扩展名
- 下载感知:统一加下载中轻量 toast(旋转图标、不锁页面),工作空间按钮级
  「下载中…」禁用;成功/失败替换为结果提示
- 新增 downloadWorkspaceFile(workflow 域独立 API):下载超时放宽到 5 分钟,
  不动 creation 共享 downloadToFile

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-21 20:09:20 +08:00
co-authored by Claude
parent 490cf4bb93
commit d48875bd7a
3 changed files with 76 additions and 21 deletions
+14
View File
@@ -1,4 +1,5 @@
import request from '/@/utils/request';
import type { RequestOptions } from '/@/utils/request';
// ===== 节点库(V2 结构,workflow 专用;creation 继续使用 creation/index.ts 的旧结构)=====
export interface NodeLibraryPresetOption {
@@ -134,3 +135,16 @@ export function getSubFlowWorkflowDetail(id: string) {
params: { id },
}) as Promise<{ code: number; message: string; data: any }>;
}
// 工作空间文件下载(首页工作流/工作空间专用):与 creation 的 downloadToFile 同一后端接口,
// 但工作空间文件可能较大(视频/音频),下载超时放宽到 5 分钟;不改动 creation 共享 API 的超时
export function downloadWorkspaceFile(data: { fileURL: string }, requestOptions?: RequestOptions) {
return request({
url: '/oss/file/downloadToBrowser',
method: 'post',
data,
responseType: 'blob',
timeout: 5 * 60 * 1000,
requestOptions,
});
}
+19 -1
View File
@@ -77,7 +77,13 @@
<span class="file-name">{{ fileNode.label }}</span>
<div v-if="fileNode.fileUrl" class="file-actions">
<button class="file-action-btn" @click.stop="handlePreviewNode(fileNode)">预览</button>
<button class="file-action-btn" @click.stop="handleDownloadNode(fileNode)">下载</button>
<button
class="file-action-btn"
:disabled="downloadingFileId === fileNode.id"
@click.stop="handleDownloadNode(fileNode)"
>
{{ downloadingFileId === fileNode.id ? '下载中…' : '下载' }}
</button>
<button class="file-action-btn file-action-btn--del" @click.stop="handleDeleteNode(fileNode)">删除</button>
</div>
</div>
@@ -120,6 +126,8 @@ interface Props {
workspaceFailed?: boolean;
// 工作空间是否已成功加载:懒加载成功后才不再触发;失败保持 false → 切回 tab 自动重试
workspaceLoaded?: boolean;
// 工作空间正在下载的文件 id:对应「下载」按钮显示下载中并禁用(按钮级感知,不影响其他功能)
downloadingFileId?: string | null;
treeNodes: TreeNode[];
treeLoading: boolean;
}
@@ -144,6 +152,7 @@ const props = withDefaults(defineProps<Props>(), {
historyLoadingMore: false,
workspaceFailed: false,
workspaceLoaded: false,
downloadingFileId: null,
});
const emit = defineEmits<Emits>();
@@ -498,6 +507,15 @@ const handleDeleteNode = (data: TreeNode) => emit('delete-node', data);
background: #dbeafe;
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
&:hover {
background: transparent;
}
}
&--del {
color: #ef4444;
+43 -20
View File
@@ -11,6 +11,7 @@
:tree-loading="treeLoading"
:workspace-failed="workspaceTreeFailed"
:workspace-loaded="workspaceTreeLoaded"
:downloading-file-id="downloadingFileId"
@menu-change="handleMenuChange"
@new-chat="handleCreateHistory"
@select-history="handleSelectHistory"
@@ -91,8 +92,9 @@
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted, onBeforeUnmount } from 'vue';
import { ref, reactive, computed, h, onMounted, onBeforeUnmount } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { Loading } from '@element-plus/icons-vue';
import Sidebar from './components/Sidebar.vue';
import MainContent from './components/MainContent.vue';
import InputBar from './components/InputBar.vue';
@@ -103,11 +105,12 @@ import type { WorkflowOutput } from './utils/flowDsl';
import { getChatModel, listModelManage } from '/@/api/settings/modelConfigV2';
import { connectSessionSocket, sendAgentStart, sendWorkflowStart, sendCancel } from './utils/wsExecute';
import { parseWsMessage, getDelta, getAnswer, getRecordId, getErrorText, getNodeEvent, getToolCallName, getToolResultText, type WorkflowNodeProgress } from './utils/wsMessage';
import { getApiErrorMessage } from '/@/utils/request';
import {
getWorkflowDetail,
deleteExecutionResult,
downloadToFile,
} from '/@/api/settings/creation';
import { downloadWorkspaceFile } from '/@/api/settings/workflow/index';
import { getSessionListV2, getSessionResults, deleteSessionV2, deleteSessionRecord, getWorkspaceResultList, deleteWorkspaceResult } from '/@/api/settings/workflow/session';
import type { ExecutionTreeItem, VOSessionInfoResult } from '/@/api/settings/workflow/session';
@@ -155,6 +158,8 @@ const activeMenu = ref('chat');
const activeHistoryId = ref<string | null>(null);
const treeLoading = ref(false);
const treeNodes = ref<TreeNode[]>([]);
// 工作空间正在下载的文件 id:按钮级下载感知,不锁页面、不影响其他功能
const downloadingFileId = ref<string | null>(null);
// 会话记录 / 工作空间结果树加载失败标记:失败态可区分、提供重试入口
const sessionHistoryFailed = ref(false);
const workspaceTreeFailed = ref(false);
@@ -238,12 +243,30 @@ const previewNode = (data: TreeNode) => {
previewDialogVisible.value = true;
};
// 通用文件下载:优先后端代理,兜底 a 标签
const downloadAsset = async (url: string, name: string) => {
// 通用文件下载:后端代理转 blob 后触发下载;文件名优先用 URL 里的原始文件名(含扩展名)
const downloadAsset = async (url: string, label: string, fileType?: string) => {
// 下载感知:下载中的轻量 toast(带旋转图标、不自动关闭),不锁页面、不影响其他功能;
// 成功/失败时关闭并替换为结果提示
const downloadingMsg = ElMessage({
message: `正在下载${label ? ` ${label}` : ''}`,
// 旋转 loading 图标:el-icon 包裹 + is-loading 触发 Element Plus 全局旋转动画
icon: () => h('span', { class: 'el-icon is-loading' }, [h(Loading)]),
duration: 0,
showClose: true,
});
try {
// 优先通过后端代理下载,避免跨域问题
const res = await downloadToFile({ fileURL: url });
const blob = res.data as Blob;
// 后端已调整:content 即下载接口所需对象名(相对路径,含前导斜杠),直接传递;
// 兼容历史完整 URL:仅去掉协议/域名,保留路径
const objectName = url.replace(/^https?:\/\/[^/]+/, '');
const r = await downloadWorkspaceFile({ fileURL: objectName }, { errorMode: 'page' });
const blob = r instanceof Blob ? r : r?.data;
// 兜底:后端 200 + JSON 错误体时 blob 不是目标文件,拒绝生成坏文件
if (!(blob instanceof Blob) || /json/i.test(blob.type)) throw new Error('服务返回异常,请重试');
const fileName = decodeURIComponent(url.split('/').pop() || '');
const type = String(fileType || '').toLowerCase();
const defaultExt = type === 'video' ? 'mp4' : type === 'audio' ? 'mp3' : type === 'text' ? 'txt' : 'html';
const fileExt = fileName.split('.').pop()?.toLowerCase() || defaultExt;
const name = fileName || `${label}.${fileExt}`;
const blobUrl = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = blobUrl;
@@ -252,23 +275,23 @@ const downloadAsset = async (url: string, name: string) => {
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(blobUrl);
} catch {
// 兜底:直接用 a 标签下载
const full = buildAssetUrl(url);
const a = document.createElement('a');
a.href = full;
a.download = name;
a.target = '_blank';
a.rel = 'noopener';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
downloadingMsg.close();
ElMessage.success(`下载完成:${name}`);
} catch (e) {
downloadingMsg.close();
ElMessage.error(getApiErrorMessage(e, '下载失败'));
}
};
const downloadNode = async (data: TreeNode) => {
if (!data.fileUrl) return;
await downloadAsset(data.fileUrl, data.label);
// 工作空间下载感知:对应文件「下载」按钮显示下载中并禁用,不锁页面、不影响其他功能
downloadingFileId.value = data.id;
try {
await downloadAsset(data.fileUrl, data.label, data.fileType);
} finally {
downloadingFileId.value = null;
}
};
const handleDeleteNode = async (data: TreeNode) => {
@@ -1253,7 +1276,7 @@ const handleOutputPreview = (msg: ChatMessage, output: WorkflowOutput) => {
const handleOutputDownload = async (msg: ChatMessage, output: WorkflowOutput) => {
if (!output.url) return;
await downloadAsset(output.url, output.name);
await downloadAsset(output.url, output.name, output.type);
};
const handleOutputDelete = async (msg: ChatMessage, output: WorkflowOutput) => {