首页工作空间下载:对象名传参适配后端相对路径、下载感知(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:
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user