diff --git a/web/src/pages/app/modules/folders.ts b/web/src/pages/app/modules/folders.ts index e11c87a..4859f47 100644 --- a/web/src/pages/app/modules/folders.ts +++ b/web/src/pages/app/modules/folders.ts @@ -63,7 +63,8 @@ async function runFolderAction(action: () => Promise) { } } -export async function loadFolders() { +export async function loadFolders(options: { reloadModels?: boolean } = {}) { + const reloadModels = options.reloadModels ?? true; const result = await api("/api/folders"); appState.folders = result.folders; const root = appState.folders.find((folder) => folder.parent_id === null); @@ -125,8 +126,10 @@ export async function loadFolders() { if (appState.selectedFolderId) { $("#folderTree").on("ready.jstree", () => { - $("#folderTree").jstree(true).select_node(String(appState.selectedFolderId)); + $("#folderTree").jstree(true).select_node(String(appState.selectedFolderId), true); }); } - await loadModels(); + if (reloadModels) { + await loadModels(); + } } diff --git a/web/src/pages/app/modules/models.ts b/web/src/pages/app/modules/models.ts index 335fe20..cfbe68c 100644 --- a/web/src/pages/app/modules/models.ts +++ b/web/src/pages/app/modules/models.ts @@ -45,6 +45,7 @@ let dictionariesLoading: Promise | null = null; let sceneModelBridgeBound = false; let pendingSceneModelRequestId = ""; let pendingSceneModelRequestTimer: number | null = null; +let visibleModels = new Map(); export function bindModelActions() { const isAdmin = getCurrentUser()?.role === "admin"; @@ -123,7 +124,7 @@ export function bindModelActions() { }); } -export async function loadModels() { +export async function loadModels(options: { preserveCards?: boolean } = {}) { const grid = document.querySelector("#modelGrid"); if (!grid || !appState.selectedFolderId) return; await loadDictionaries(); @@ -153,42 +154,85 @@ export async function loadModels() { await loadModels(); } }); - grid.innerHTML = result.items.map(renderModelCard).join("") || `
当前目录暂无模型
`; - grid.querySelectorAll("[data-action='preview']").forEach((button) => { - const model = result.items.find((item) => item.id === Number(button.dataset.id)); - if (model) { - button.addEventListener("click", async () => { + visibleModels = new Map(result.items.map((item) => [item.id, item])); + renderModelGrid(grid, result.items, options.preserveCards); + bindModelGridActions(grid); +} + +function bindModelGridActions(grid: HTMLDivElement) { + if (grid.dataset.actionsBound === "true") return; + grid.dataset.actionsBound = "true"; + grid.addEventListener("click", async (event) => { + const target = event.target instanceof Element ? event.target : null; + const button = target?.closest("button[data-action][data-id]"); + if (!button || !grid.contains(button)) return; + const id = Number(button.dataset.id); + const model = visibleModels.get(id); + try { + switch (button.dataset.action) { + case "preview": { + if (!model) return; const { openModelPreview } = await import("./preview"); openModelPreview(model, loadModels); - }); + break; + } + case "edit": + await editModel(id); + break; + case "import": { + if (!model) return; + const { createImportScenePayload, emitImportScenePayload } = await import("./preview"); + const payload = createImportScenePayload({ + model, + basePointEnabled: false, + basePoint: { x: 0, y: 0, z: 0, rx: 0, ry: 0, rz: 0 }, + selectedOperationIndex: null + }); + emitImportScenePayload(payload); + notify("导入场景数据已输出"); + break; + } + case "delete": + await deleteModel(id); + break; + } + } catch (error) { + notifyError(error); } }); - grid.querySelectorAll("[data-action='edit']").forEach((button) => { - button.addEventListener("click", () => editModel(Number(button.dataset.id))); - }); - grid.querySelectorAll("[data-action='import']").forEach((button) => { - const model = result.items.find((item) => item.id === Number(button.dataset.id)); - if (!model) return; - button.addEventListener("click", async () => { - const { createImportScenePayload, emitImportScenePayload } = await import("./preview"); - const payload = createImportScenePayload({ - model, - basePointEnabled: false, - basePoint: { x: 0, y: 0, z: 0, rx: 0, ry: 0, rz: 0 }, - selectedOperationIndex: null - }); - emitImportScenePayload(payload); - notify("导入场景数据已输出"); - }); - }); - grid.querySelectorAll("[data-action='delete']").forEach((button) => { - button.addEventListener("click", () => deleteModel(Number(button.dataset.id))); - }); } -async function reloadFoldersAndModels() { +function renderModelGrid(grid: HTMLDivElement, items: ModelItem[], preserveCards = false) { + if (!preserveCards) { + grid.innerHTML = items.map(renderModelCard).join("") || `
当前目录暂无模型
`; + return; + } + if (items.length === 0) { + grid.innerHTML = `
当前目录暂无模型
`; + return; + } + const existingCards = new Map( + Array.from(grid.querySelectorAll(".model-card[data-model-id]")) + .map((card) => [Number(card.dataset.modelId), card]) + ); + const fragment = document.createDocumentFragment(); + for (const item of items) { + const existingCard = existingCards.get(item.id); + if (existingCard) { + fragment.append(existingCard); + continue; + } + const template = document.createElement("template"); + template.innerHTML = renderModelCard(item).trim(); + fragment.append(template.content); + } + grid.replaceChildren(fragment); +} + +async function reloadFoldersAndModels(preserveModelCards = false) { const { loadFolders } = await import("./folders"); - await loadFolders(); + await loadFolders({ reloadModels: false }); + await loadModels({ preserveCards: preserveModelCards }); } async function loadDictionaries(force = false) { @@ -786,5 +830,5 @@ async function deleteModel(id: number) { const confirmed = await confirmDialog("确认删除该模型?"); if (!confirmed) return; await api(`/api/models/${id}`, { method: "DELETE" }); - await reloadFoldersAndModels(); + await reloadFoldersAndModels(true); }