From 07bdc4a3b6b768ce4803695dbf117a69da82f013 Mon Sep 17 00:00:00 2001 From: Mahdi Date: Sat, 3 Oct 2026 17:45:08 +0330 Subject: [PATCH] Show percent progress bar during long Excel imports. Co-authored-by: Cursor --- apps/web/src/components/ExcelImportFlow.vue | 37 ++++++++++-- apps/web/src/components/ui/UiBusyOverlay.vue | 59 +++++++++++++++++++- 2 files changed, 88 insertions(+), 8 deletions(-) diff --git a/apps/web/src/components/ExcelImportFlow.vue b/apps/web/src/components/ExcelImportFlow.vue index 0f35103..21d9b22 100644 --- a/apps/web/src/components/ExcelImportFlow.vue +++ b/apps/web/src/components/ExcelImportFlow.vue @@ -25,6 +25,8 @@ const mapping = reactive>({}); const submitting = ref(false); const phase = ref<'idle' | 'preview' | 'import'>('idle'); const progressLabel = ref(''); +/** درصد ۰–۱۰۰؛ در مرحلهٔ خواندن فایل null */ +const progressPct = ref(null); const requiredKeys = computed( () => preview.value?.fields.filter((f) => f.required).map((f) => f.key) ?? [], @@ -41,6 +43,9 @@ const busyMessage = computed(() => { } return 'در حال خواندن و بررسی فایل اکسل…'; }); +const busyProgress = computed(() => + phase.value === 'import' ? progressPct.value : null, +); function pick() { if (props.disabled || submitting.value) return; @@ -53,6 +58,7 @@ function reset() { submitting.value = false; phase.value = 'idle'; progressLabel.value = ''; + progressPct.value = null; for (const key of Object.keys(mapping)) delete mapping[key]; } @@ -112,9 +118,21 @@ async function postImportChunk(opts: { return data; } +function updateImportProgress(processed: number, total: number) { + if (total <= 0) { + progressPct.value = null; + progressLabel.value = 'در حال ورود اطلاعات از اکسل…'; + return; + } + const pct = Math.max(0, Math.min(100, Math.round((processed / total) * 100))); + progressPct.value = pct; + progressLabel.value = `ورود فایل: ${processed.toLocaleString('fa-IR')} از ${total.toLocaleString('fa-IR')} ردیف`; +} + async function runChunkedImport(file: File, map?: Record) { phase.value = 'import'; - progressLabel.value = 'در حال ورود اطلاعات از اکسل…'; + progressLabel.value = 'در حال آماده‌سازی و خواندن فایل…'; + progressPct.value = 0; let acc: ImportContactsResult = { created: 0, skipped: 0, errors: [] }; let sessionId: string | undefined; @@ -134,9 +152,7 @@ async function runChunkedImport(file: File, map?: Record) { const total = part.total ?? 0; const processed = part.processed ?? 0; - if (total > 0) { - progressLabel.value = `ورود چندمرحله‌ای: ${processed.toLocaleString('fa-IR')} از ${total.toLocaleString('fa-IR')} ردیف`; - } + updateImportProgress(processed, total); // خطای سراسری (مثلاً سقف ردیف) — ادامه نده if ( @@ -145,7 +161,10 @@ async function runChunkedImport(file: File, map?: Record) { break; } - if (!part.hasMore || !part.sessionId) break; + if (!part.hasMore || !part.sessionId) { + if (total > 0) updateImportProgress(total, total); + break; + } sessionId = part.sessionId; offset = part.nextOffset ?? processed; } @@ -228,6 +247,7 @@ async function confirmMapping() { phase.value = 'idle'; submitting.value = false; progressLabel.value = ''; + progressPct.value = null; emit('settled'); } } @@ -247,7 +267,12 @@ async function confirmMapping() { /> - +

diff --git a/apps/web/src/components/ui/UiBusyOverlay.vue b/apps/web/src/components/ui/UiBusyOverlay.vue index e6a9112..f3e7696 100644 --- a/apps/web/src/components/ui/UiBusyOverlay.vue +++ b/apps/web/src/components/ui/UiBusyOverlay.vue @@ -1,16 +1,31 @@