增加前端运行时后端地址配置

This commit is contained in:
zhangshun
2026-06-02 13:32:08 +08:00
parent 8fb45bd42a
commit e2e7aa5f06
7 changed files with 73 additions and 6 deletions

View File

@@ -7,6 +7,7 @@
</head>
<body>
<div id="app"></div>
<script src="/model-library-config.js"></script>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@@ -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: ""
};

52
web/src/config/runtime.ts Normal file
View File

@@ -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);
}

View File

@@ -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
? `<img src="${item.thumbnail_url}" alt="" />`
? `<img src="${resolveStorageUrl(item.thumbnail_url)}" alt="" />`
: `<div class="thumb-placeholder">暂无预览图</div>`;
return `
<article class="model-card" data-model-id="${item.id}">

View File

@@ -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,

View File

@@ -1,4 +1,5 @@
import { getToken } from "./authState";
import { resolveApiUrl } from "../config/runtime";
function authHeaders(): Record<string, string> {
const token = getToken();
@@ -7,7 +8,7 @@ function authHeaders(): Record<string, string> {
export async function api<T>(url: string, options: RequestInit = {}): Promise<T> {
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" } : {}),

View File

@@ -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) {