diff --git a/web/src/pages/app/modules/models.ts b/web/src/pages/app/modules/models.ts index a41fa8b..ebc8250 100644 --- a/web/src/pages/app/modules/models.ts +++ b/web/src/pages/app/modules/models.ts @@ -22,11 +22,31 @@ type UploadModelPayload = { }; const defaultOperationTree = JSON.stringify({ OperationTree: "[]" }); +const requestSceneModelMessageType = "DMT_MODEL_LIBRARY_REQUEST_SCENE_MODEL"; +const sceneModelMessageType = "DMT_MODEL_LIBRARY_SCENE_MODEL"; +const sceneModelErrorMessageType = "DMT_MODEL_LIBRARY_SCENE_MODEL_ERROR"; + +type SceneModelMessagePayload = { + modelName?: string; + fileName?: string; + modelBuffer?: ArrayBuffer; + operationTreeJson?: string; +}; + +type SceneModelMessage = { + type?: string; + payload?: SceneModelMessagePayload; + message?: string; +}; + let dictionariesLoaded = false; let dictionariesLoading: Promise | null = null; +let sceneModelBridgeBound = false; export function bindModelActions() { const isAdmin = getCurrentUser()?.role === "admin"; + bindSceneModelBridge(); + ensureImportSceneModelButton(); if (isAdmin) { document.querySelector("#manageUsersBtn")?.addEventListener("click", async () => { try { @@ -242,8 +262,90 @@ function syncSelectedFolderActions() { const canWrite = Boolean(currentFolder?.permissions.write); const addModelBtn = document.querySelector("#addModelBtn"); const addProcessModelBtn = document.querySelector("#addProcessModelBtn"); + const importSceneModelBtn = document.querySelector("#importSceneModelBtn"); if (addModelBtn) addModelBtn.hidden = !canWrite; if (addProcessModelBtn) addProcessModelBtn.hidden = !canWrite; + if (importSceneModelBtn) importSceneModelBtn.hidden = !canWrite; +} + +function ensureImportSceneModelButton() { + if (document.querySelector("#importSceneModelBtn")) return; + const addModelBtn = document.querySelector("#addModelBtn"); + if (!addModelBtn) return; + const button = document.createElement("button"); + button.id = "importSceneModelBtn"; + button.type = "button"; + button.textContent = "导入场景模型"; + button.addEventListener("click", requestSceneModelImport); + addModelBtn.insertAdjacentElement("afterend", button); +} + +function bindSceneModelBridge() { + if (sceneModelBridgeBound) return; + sceneModelBridgeBound = true; + window.addEventListener("message", (event) => { + const data = event.data as SceneModelMessage; + if (data?.type !== sceneModelMessageType && data?.type !== sceneModelErrorMessageType) return; + if (!isAllowedParentOrigin(event.origin)) return; + void handleSceneModelMessage(data); + }); +} + +function requestSceneModelImport() { + if (!appState.selectedFolderId) { + notify("请先选择目录"); + return; + } + if (!window.parent || window.parent === window) { + notify("请在 DMT 主程序中使用导入场景模型"); + return; + } + window.parent.postMessage({ + type: requestSceneModelMessageType + }, resolveParentOrigin()); + notify("已请求主程序导出当前选中模型"); +} + +async function handleSceneModelMessage(data: SceneModelMessage) { + if (data.type === sceneModelErrorMessageType) { + notifyError(data.message || "主程序导出模型失败"); + return; + } + + const payload = data.payload; + if (!payload?.modelBuffer) { + notifyError("主程序返回的模型数据为空"); + return; + } + + const fileName = ensureGlbFileName(payload.fileName || payload.modelName || "scene-model.glb"); + const file = new File([payload.modelBuffer], fileName, { type: "model/gltf-binary" }); + await openProcessModelUploadDialog({ + file, + modelName: payload.modelName || modelNameFromFile(fileName), + operationTreeJson: payload.operationTreeJson || defaultOperationTree + }); +} + +function isAllowedParentOrigin(origin: string) { + const expectedOrigin = resolveParentOrigin(); + return expectedOrigin === "*" || origin === expectedOrigin; +} + +function resolveParentOrigin() { + try { + const meta = import.meta as ImportMeta & { env?: Record }; + const configuredOrigin = meta.env?.VITE_DMT_PARENT_ORIGIN; + if (configuredOrigin) return configuredOrigin; + if (document.referrer) return new URL(document.referrer).origin; + } catch { + return "*"; + } + return "*"; +} + +function ensureGlbFileName(fileName: string) { + return fileName.toLowerCase().endsWith(".glb") ? fileName : `${fileName}.glb`; } export async function uploadModelToBackend(payload: UploadModelPayload) {