diff --git a/web/index.html b/web/index.html index 1fcfc01..6860bd1 100644 --- a/web/index.html +++ b/web/index.html @@ -7,6 +7,7 @@
+ diff --git a/web/public/model-library-config.js b/web/public/model-library-config.js new file mode 100644 index 0000000..2353a2a --- /dev/null +++ b/web/public/model-library-config.js @@ -0,0 +1,9 @@ +window.__DMT_MODEL_LIBRARY_CONFIG__ = { + // 后端服务根地址。为空时使用当前页面域名,例如 /api/models。 + // 示例:"https://model-api.example.com" + API_BASE_URL: "", + + // 模型文件和缩略图服务根地址。为空时优先复用 API_BASE_URL。 + // 示例:"https://model-api.example.com" + STORAGE_BASE_URL: "" +}; diff --git a/web/src/config/runtime.ts b/web/src/config/runtime.ts new file mode 100644 index 0000000..021927b --- /dev/null +++ b/web/src/config/runtime.ts @@ -0,0 +1,52 @@ +type RuntimeConfig = { + API_BASE_URL?: string; + STORAGE_BASE_URL?: string; +}; + +declare global { + interface Window { + __DMT_MODEL_LIBRARY_CONFIG__?: RuntimeConfig; + } +} + +function runtimeConfig() { + return window.__DMT_MODEL_LIBRARY_CONFIG__ ?? {}; +} + +function cleanBaseUrl(value?: string) { + return value?.trim().replace(/\/+$/g, "") ?? ""; +} + +function isAbsoluteUrl(url: string) { + return /^https?:\/\//i.test(url); +} + +function joinUrl(baseUrl: string, path: string) { + if (!baseUrl || isAbsoluteUrl(path)) return path; + return `${baseUrl}${path.startsWith("/") ? "" : "/"}${path}`; +} + +function joinApiUrl(baseUrl: string, path: string) { + if (!baseUrl || isAbsoluteUrl(path)) return path; + + try { + const base = new URL(baseUrl); + if (base.pathname.replace(/\/+$/g, "") === "/api" && path.startsWith("/api/")) { + return joinUrl(baseUrl, path.slice(4)); + } + } catch { + // 非完整 URL 时按普通路径拼接。 + } + + return joinUrl(baseUrl, path); +} + +export function resolveApiUrl(path: string) { + return joinApiUrl(cleanBaseUrl(runtimeConfig().API_BASE_URL), path); +} + +export function resolveStorageUrl(path: string) { + const storageBaseUrl = cleanBaseUrl(runtimeConfig().STORAGE_BASE_URL); + const apiBaseUrl = cleanBaseUrl(runtimeConfig().API_BASE_URL); + return joinUrl(storageBaseUrl || apiBaseUrl, path); +} diff --git a/web/src/pages/app/modules/models.ts b/web/src/pages/app/modules/models.ts index 6fa6be9..4008024 100644 --- a/web/src/pages/app/modules/models.ts +++ b/web/src/pages/app/modules/models.ts @@ -5,6 +5,7 @@ import type { DictionaryResponse, ModelItem, ModelListResponse } from "../../../ import { confirmDialog, formDialog, notify, notifyError } from "../../../ui/dialogs"; import { downloadFileFromUrl } from "../../../utils/fileDownload"; import { escapeHtml, modelNameFromFile } from "../../../utils/format"; +import { resolveStorageUrl } from "../../../config/runtime"; import { appState } from "../appState"; type UploadFormState = { @@ -287,7 +288,7 @@ function renderModelCard(item: ModelItem) { const canWrite = item.permissions.write; const prop = item.properties ?? {}; const thumb = item.thumbnail_url - ? `` + ? `` : `
暂无预览图
`; return `
diff --git a/web/src/pages/app/modules/preview.ts b/web/src/pages/app/modules/preview.ts index 8a9b282..e5e3fe7 100644 --- a/web/src/pages/app/modules/preview.ts +++ b/web/src/pages/app/modules/preview.ts @@ -9,6 +9,7 @@ import { downloadFileFromUrl } from "../../../utils/fileDownload"; import { escapeHtml, formatBytes } from "../../../utils/format"; import { notify, notifyError } from "../../../ui/dialogs"; import { api } from "../../../services/api"; +import { resolveStorageUrl } from "../../../config/runtime"; type PreviewRuntime = { renderer: WebGLRenderer; @@ -146,7 +147,7 @@ export function openModelPreview( ) { const context = beginPreviewContext(); const canWrite = model.permissions.write; - const url = model.file_url; + const url = resolveStorageUrl(model.file_url); const operations = parseModelOperations(model.operation_tree); const popup = w2popup.open({ title: `模型预览 - ${escapeHtml(model.name)}【${formatBytes(model.file_size)}】`, @@ -300,7 +301,7 @@ function buildParentImportScenePayload(payload: PreviewImportScenePayload): Pare id: payload.model.id, name: payload.model.name, original_filename: payload.model.original_filename, - file_url: payload.model.file_url + file_url: resolveStorageUrl(payload.model.file_url) }, modelUrl: payload.modelUrl, basePointEnabled: payload.basePointEnabled, @@ -739,7 +740,7 @@ function buildImportScenePayload(options: { }): PreviewImportScenePayload { return { model: options.model, - modelUrl: options.model.file_url, + modelUrl: resolveStorageUrl(options.model.file_url), basePointEnabled: options.basePointEnabled, basePoint: options.basePoint, rawOperationTree: options.model.operation_tree, diff --git a/web/src/services/api.ts b/web/src/services/api.ts index 359b009..8d40fb2 100644 --- a/web/src/services/api.ts +++ b/web/src/services/api.ts @@ -1,4 +1,5 @@ import { getToken } from "./authState"; +import { resolveApiUrl } from "../config/runtime"; function authHeaders(): Record { const token = getToken(); @@ -7,7 +8,7 @@ function authHeaders(): Record { export async function api(url: string, options: RequestInit = {}): Promise { const hasBody = options.body !== undefined && options.body !== null; - const response = await fetch(url, { + const response = await fetch(resolveApiUrl(url), { ...options, headers: { ...(hasBody && !(options.body instanceof FormData) ? { "Content-Type": "application/json" } : {}), diff --git a/web/src/utils/fileDownload.ts b/web/src/utils/fileDownload.ts index 7521b84..de50df0 100644 --- a/web/src/utils/fileDownload.ts +++ b/web/src/utils/fileDownload.ts @@ -1,5 +1,7 @@ +import { resolveStorageUrl } from "../config/runtime"; + export async function downloadFileFromUrl(fileUrl: string, fileName: string) { - const response = await fetch(fileUrl, { + const response = await fetch(resolveStorageUrl(fileUrl), { credentials: "include" }); if (!response.ok) {