diff --git a/web/src/pages/app/dialogs.css b/web/src/pages/app/dialogs.css index 43dd822..5bb9179 100644 --- a/web/src/pages/app/dialogs.css +++ b/web/src/pages/app/dialogs.css @@ -95,6 +95,74 @@ opacity: 0.72; } +.upload-progress-overlay { + position: fixed; + inset: 0; + z-index: 100000; + display: grid; + place-items: center; + padding: 24px; + background: rgba(15, 23, 42, 0.58); +} + +.upload-progress-overlay[hidden] { + display: none; +} + +.upload-progress-panel { + width: min(460px, 100%); + display: grid; + gap: 12px; + padding: 22px 24px; + border: 1px solid #dbe5ec; + border-radius: 8px; + background: #ffffff; + box-shadow: 0 18px 48px rgba(15, 23, 42, 0.24); +} + +.upload-progress-title { + color: #1c2733; + font-size: 18px; + font-weight: 700; +} + +.upload-progress-message { + color: #405469; + font-size: 14px; + line-height: 1.5; + word-break: break-all; +} + +.upload-progress-track { + height: 10px; + overflow: hidden; + border-radius: 999px; + background: #e8eef3; +} + +.upload-progress-bar { + width: 0; + height: 100%; + border-radius: inherit; + background: #1f6f8b; + transition: width 180ms ease; +} + +.upload-progress-meta { + color: #27613a; + font-size: 13px; + font-weight: 700; +} + +.upload-progress-file { + max-height: 48px; + overflow: auto; + color: #647586; + font-size: 12px; + line-height: 1.4; + 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 a008e73..c55dbfc 100644 --- a/web/src/pages/app/modules/models.ts +++ b/web/src/pages/app/modules/models.ts @@ -529,10 +529,19 @@ async function openUploadModelDialog() { model: String(form.get("model") ?? "") }; let completedCount = 0; + const totalCount = state.files.length; setUploadDialogBusy(state, true); + showUploadProgressOverlay(totalCount); try { for (const [index, file] of state.files.entries()) { - updateUploadStatus(`正在上传 ${index + 1}/${state.files.length}:${file.name}`); + const uploadingMessage = `正在上传 ${index + 1}/${totalCount}:${file.name}`; + updateUploadStatus(uploadingMessage); + updateUploadProgressOverlay({ + total: totalCount, + completed: completedCount, + message: uploadingMessage, + fileName: file.name + }); await waitForUploadStatusPaint(); await uploadModelToBackend({ file, @@ -544,12 +553,20 @@ async function openUploadModelDialog() { properties }); completedCount += 1; - updateUploadStatus(`已完成 ${completedCount}/${state.files.length}`); + const completedMessage = `已完成 ${completedCount}/${totalCount}`; + updateUploadStatus(completedMessage); + updateUploadProgressOverlay({ + total: totalCount, + completed: completedCount, + message: completedMessage, + fileName: file.name + }); } } catch (error) { - updateUploadStatus(`上传失败,已完成 ${completedCount}/${state.files.length},请处理后重试。`); + updateUploadStatus(`上传失败,已完成 ${completedCount}/${totalCount},请处理后重试。`); throw error; } finally { + hideUploadProgressOverlay(); if (completedCount < state.files.length) { setUploadDialogBusy(state, false); } @@ -704,6 +721,58 @@ function waitForUploadStatusPaint() { }); } +function showUploadProgressOverlay(total: number) { + let overlay = document.querySelector("#modelUploadProgressOverlay"); + if (!overlay) { + overlay = document.createElement("div"); + overlay.id = "modelUploadProgressOverlay"; + overlay.className = "upload-progress-overlay"; + overlay.setAttribute("role", "status"); + overlay.setAttribute("aria-live", "polite"); + overlay.innerHTML = ` +
+
模型上传中
+
正在准备上传...
+
+
+
+
0/${total}
+
+
+ `; + document.body.appendChild(overlay); + } + overlay.hidden = false; + updateUploadProgressOverlay({ + total, + completed: 0, + message: "正在准备上传...", + fileName: "" + }); +} + +function updateUploadProgressOverlay(options: { + total: number; + completed: number; + message: string; + fileName: string; +}) { + const message = document.querySelector("#modelUploadProgressMessage"); + const meta = document.querySelector("#modelUploadProgressMeta"); + const file = document.querySelector("#modelUploadProgressFile"); + const bar = document.querySelector("#modelUploadProgressBar"); + const percent = options.total > 0 ? Math.round((options.completed / options.total) * 100) : 0; + if (message) message.textContent = options.message; + if (meta) meta.textContent = `${options.completed}/${options.total}`; + if (file) file.textContent = options.fileName; + if (bar) bar.style.width = `${percent}%`; +} + +function hideUploadProgressOverlay() { + const overlay = document.querySelector("#modelUploadProgressOverlay"); + if (overlay) overlay.hidden = true; +} + function setUploadDialogBusy(state: UploadFormState, busy: boolean) { const form = document.querySelector("#modelUploadPopupForm"); if (!form) return;