diff --git a/web/src/pages/app/dialogs.css b/web/src/pages/app/dialogs.css index 6342808..291d814 100644 --- a/web/src/pages/app/dialogs.css +++ b/web/src/pages/app/dialogs.css @@ -64,6 +64,21 @@ color: #405469; } +.selected-file-summary { + display: block; + color: #1f6f8b; + font-weight: 600; +} + +.selected-file-list { + display: block; + max-height: 58px; + overflow: auto; + color: #405469; + line-height: 1.45; + word-break: break-all; +} + .upload-target-path { display: grid; grid-template-columns: 64px minmax(0, 1fr); diff --git a/web/src/pages/app/modules/models.ts b/web/src/pages/app/modules/models.ts index 4008024..1c94db5 100644 --- a/web/src/pages/app/modules/models.ts +++ b/web/src/pages/app/modules/models.ts @@ -10,6 +10,7 @@ import { appState } from "../appState"; type UploadFormState = { file: File | null; + files: File[]; }; type UploadModelPayload = { @@ -481,7 +482,7 @@ async function openUploadModelDialog() { return; } - const state: UploadFormState = { file: null }; + const state: UploadFormState = { file: null, files: [] }; await loadDictionaries(); const result = await formDialog({ title: "增加模型", @@ -493,9 +494,9 @@ async function openUploadModelDialog() { @@ -513,25 +514,30 @@ async function openUploadModelDialog() { `, onOpen: () => bindUploadDialogEvents(state), onSubmit: async () => { - if (!state.file) { + if (state.files.length === 0) { throw new Error("请选择 .glb 模型文件"); } const name = document.querySelector("#modelUploadName")?.value.trim(); - if (!name) { + if (state.files.length === 1 && !name) { throw new Error("模型名称不能为空"); } const form = new FormData(document.querySelector("#modelUploadPopupForm")!); - await uploadModelToBackend({ - file: state.file, - name, - fileName: state.file.name, - brandName: String(form.get("brandName") ?? ""), - typeName: String(form.get("typeName") ?? ""), - operationTree: defaultOperationTree, - properties: { - model: String(form.get("model") ?? "") - } - }); + const brandName = String(form.get("brandName") ?? ""); + const typeName = String(form.get("typeName") ?? ""); + const properties = { + model: String(form.get("model") ?? "") + }; + for (const file of state.files) { + await uploadModelToBackend({ + file, + name: state.files.length === 1 ? name ?? "" : modelNameFromFile(file.name), + fileName: file.name, + brandName, + typeName, + operationTree: defaultOperationTree, + properties + }); + } return true; } }); @@ -552,7 +558,7 @@ export async function openProcessModelUploadDialog(input?: { return; } - const state: UploadFormState = { file: input?.file ?? null }; + const state: UploadFormState = { file: input?.file ?? null, files: input?.file ? [input.file] : [] }; await loadDictionaries(); const initialName = input?.modelName || (input?.file ? modelNameFromFile(input.file.name) : ""); const result = await formDialog({ @@ -605,21 +611,48 @@ function bindUploadDialogEvents(state: UploadFormState) { const nameInput = document.querySelector("#modelUploadName")!; const selectedFileName = document.querySelector("#selectedFileName")!; - const selectFile = (file: File) => { - if (!file.name.toLowerCase().endsWith(".glb")) { + const renderSelectedFiles = () => { + if (state.files.length === 0) { + selectedFileName.textContent = "未选择文件"; + nameInput.disabled = false; + nameInput.value = ""; + nameInput.placeholder = "选择文件后自动填入"; + return; + } + + if (state.files.length === 1) { + const [file] = state.files; + selectedFileName.textContent = file.name; + nameInput.disabled = false; + nameInput.value = modelNameFromFile(file.name); + nameInput.placeholder = "选择文件后自动填入"; + return; + } + + selectedFileName.innerHTML = ` + 已选择 ${state.files.length} 个模型,名称按文件名自动生成 + ${state.files.map((file) => escapeHtml(file.name)).join("
")}
+ `; + nameInput.disabled = true; + nameInput.value = `${state.files.length} 个模型按文件名自动命名`; + nameInput.placeholder = ""; + }; + + const selectFiles = (files: FileList | File[]) => { + const selectedFiles = Array.from(files); + if (selectedFiles.length === 0) return; + const invalidFiles = selectedFiles.filter((file) => !file.name.toLowerCase().endsWith(".glb")); + if (invalidFiles.length > 0) { notify("当前阶段只允许上传 .glb 模型"); return; } - state.file = file; - selectedFileName.textContent = file.name; - if (!nameInput.value.trim()) { - nameInput.value = modelNameFromFile(file.name); - } + state.files = selectedFiles; + state.file = selectedFiles[0] ?? null; + renderSelectedFiles(); }; fileInput.addEventListener("change", () => { - const file = fileInput.files?.[0]; - if (file) selectFile(file); + if (fileInput.files) selectFiles(fileInput.files); }); dropZone.addEventListener("click", (event) => { @@ -636,8 +669,8 @@ function bindUploadDialogEvents(state: UploadFormState) { dropZone.addEventListener("drop", (event) => { event.preventDefault(); dropZone.classList.remove("is-dragover"); - const file = event.dataTransfer?.files?.[0]; - if (file) selectFile(file); + const files = event.dataTransfer?.files; + if (files) selectFiles(files); }); }