From b1ef3deb035800e1b7a5a930679dd83546949342 Mon Sep 17 00:00:00 2001 From: zhangshun <453905631@qq.com> Date: Mon, 1 Jun 2026 14:34:44 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=A8=A1=E5=9E=8B=E5=BA=93?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E4=B8=8B=E8=BD=BD=E5=85=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/pages/app/dialogs.css | 10 ++++++++++ web/src/pages/app/modules/models.ts | 6 ++++++ web/src/pages/app/modules/preview.ts | 17 +++++++++++++++-- web/src/utils/fileDownload.ts | 28 ++++++++++++++++++++++++++++ 4 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 web/src/utils/fileDownload.ts diff --git a/web/src/pages/app/dialogs.css b/web/src/pages/app/dialogs.css index 55b5dcb..6342808 100644 --- a/web/src/pages/app/dialogs.css +++ b/web/src/pages/app/dialogs.css @@ -236,6 +236,16 @@ padding: 8px; } +.preview-scene-actions { + display: flex; + align-items: center; + gap: 8px; +} + +.preview-scene-actions > button { + flex: 1; +} + .preview-scene-body > button { width: 100%; } diff --git a/web/src/pages/app/modules/models.ts b/web/src/pages/app/modules/models.ts index a734bf3..70becec 100644 --- a/web/src/pages/app/modules/models.ts +++ b/web/src/pages/app/modules/models.ts @@ -3,6 +3,7 @@ import { renderPagination } from "../../../components/pagination"; import { getCurrentUser } from "../../../services/authState"; import type { DictionaryResponse, ModelItem, ModelListResponse } from "../../../types"; import { confirmDialog, formDialog, notify, notifyError } from "../../../ui/dialogs"; +import { downloadFileFromUrl } from "../../../utils/fileDownload"; import { escapeHtml, modelNameFromFile } from "../../../utils/format"; import { appState } from "../appState"; @@ -192,6 +193,10 @@ function bindModelGridActions(grid: HTMLDivElement) { notify("导入场景数据已输出"); break; } + case "download": + if (!model) return; + await downloadFileFromUrl(model.file_url, model.original_filename || model.name); + break; case "delete": await deleteModel(id); break; @@ -293,6 +298,7 @@ function renderModelCard(item: ModelItem) { ${thumb}
+ ${canWrite ? `` : ""} ${canWrite ? `` : ""} diff --git a/web/src/pages/app/modules/preview.ts b/web/src/pages/app/modules/preview.ts index 411403e..d087ada 100644 --- a/web/src/pages/app/modules/preview.ts +++ b/web/src/pages/app/modules/preview.ts @@ -5,6 +5,7 @@ import type { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js"; import type { ModelItem } from "../../../types"; import type { OperationTree, OperationTreeDB } from "../../../types/OperationTree"; import { P_OPERATION } from "../../../types/OPERATION_BaseClass"; +import { downloadFileFromUrl } from "../../../utils/fileDownload"; import { escapeHtml, formatBytes } from "../../../utils/format"; import { notify, notifyError } from "../../../ui/dialogs"; import { api } from "../../../services/api"; @@ -153,13 +154,14 @@ export function openModelPreview(
-
+
+ +
-
@@ -199,6 +201,7 @@ export function openModelPreview( console.log(popup) bindProcessPlaceholder(operations); bindImportScene(model, operations, options.onImportScene); + bindDownloadModel(model); if (canWrite) bindThumbnailCapture(model.id, onThumbnailSaved); requestAnimationFrame(() => { initPreview(url, context).catch((error) => { @@ -580,6 +583,16 @@ function bindImportScene( }); } +function bindDownloadModel(model: ModelItem) { + document.querySelector("#previewDownloadBtn")?.addEventListener("click", async () => { + try { + await downloadFileFromUrl(model.file_url, model.original_filename || model.name); + } catch (error) { + notifyError(error); + } + }); +} + function resolveBasePoint(basePoint?: PreviewBasePoint): PreviewResolvedBasePoint { return { x: basePoint?.x ?? zeroBasePoint.x, diff --git a/web/src/utils/fileDownload.ts b/web/src/utils/fileDownload.ts new file mode 100644 index 0000000..7521b84 --- /dev/null +++ b/web/src/utils/fileDownload.ts @@ -0,0 +1,28 @@ +export async function downloadFileFromUrl(fileUrl: string, fileName: string) { + const response = await fetch(fileUrl, { + credentials: "include" + }); + if (!response.ok) { + throw new Error(`文件下载失败:${response.status} ${response.statusText}`); + } + + const blob = await response.blob(); + const objectUrl = URL.createObjectURL(blob); + try { + const anchor = document.createElement("a"); + anchor.href = objectUrl; + anchor.download = ensureGlbFileName(fileName); + anchor.rel = "noopener"; + anchor.style.display = "none"; + document.body.appendChild(anchor); + anchor.click(); + anchor.remove(); + } finally { + window.setTimeout(() => URL.revokeObjectURL(objectUrl), 1000); + } +} + +function ensureGlbFileName(fileName: string) { + const name = fileName.trim() || "model.glb"; + return name.toLowerCase().endsWith(".glb") ? name : `${name}.glb`; +}