Show percent progress bar during long Excel imports.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -25,6 +25,8 @@ const mapping = reactive<Record<string, string>>({});
|
||||
const submitting = ref(false);
|
||||
const phase = ref<'idle' | 'preview' | 'import'>('idle');
|
||||
const progressLabel = ref('');
|
||||
/** درصد ۰–۱۰۰؛ در مرحلهٔ خواندن فایل null */
|
||||
const progressPct = ref<number | null>(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<string, string>) {
|
||||
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<string, string>) {
|
||||
|
||||
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<string, string>) {
|
||||
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() {
|
||||
/>
|
||||
</span>
|
||||
|
||||
<UiBusyOverlay :open="busyOpen" :message="busyMessage" nest />
|
||||
<UiBusyOverlay
|
||||
:open="busyOpen"
|
||||
:message="busyMessage"
|
||||
:progress="busyProgress"
|
||||
nest
|
||||
/>
|
||||
|
||||
<UiModal :open="!!preview" title="تطبیق ستونهای اکسل" @close="closeMapper">
|
||||
<p class="lead">
|
||||
|
||||
@@ -1,16 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
withDefaults(
|
||||
import { computed } from 'vue';
|
||||
import { toFaDigits } from '@/lib/jalali';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
open: boolean;
|
||||
message?: string;
|
||||
/** وقتی true باشد روی مودال هم دیده میشود */
|
||||
nest?: boolean;
|
||||
/** درصد پیشرفت ۰ تا ۱۰۰؛ اگر null باشد نوار نشان داده نمیشود */
|
||||
progress?: number | null;
|
||||
}>(),
|
||||
{
|
||||
message: 'لطفاً صبر کنید…',
|
||||
nest: false,
|
||||
progress: null,
|
||||
},
|
||||
);
|
||||
|
||||
const showBar = computed(
|
||||
() => props.progress != null && Number.isFinite(props.progress),
|
||||
);
|
||||
const pct = computed(() => {
|
||||
if (!showBar.value) return 0;
|
||||
return Math.max(0, Math.min(100, Math.round(props.progress!)));
|
||||
});
|
||||
const pctLabel = computed(() => toFaDigits(String(pct.value)));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -22,10 +37,19 @@ withDefaults(
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
aria-busy="true"
|
||||
:aria-valuenow="showBar ? pct : undefined"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
>
|
||||
<div class="card">
|
||||
<span class="spinner" aria-hidden="true" />
|
||||
<p>{{ message }}</p>
|
||||
<div v-if="showBar" class="progress-wrap">
|
||||
<div class="progress-track" aria-hidden="true">
|
||||
<div class="progress-fill" :style="{ width: `${pct}%` }" />
|
||||
</div>
|
||||
<strong class="progress-pct">{{ pctLabel }}٪</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
@@ -51,7 +75,8 @@ withDefaults(
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
gap: 0.85rem;
|
||||
min-width: min(18rem, 100%);
|
||||
min-width: min(20rem, 100%);
|
||||
max-width: 22rem;
|
||||
padding: 1.35rem 1.5rem;
|
||||
border-radius: 16px;
|
||||
background: #fff;
|
||||
@@ -67,6 +92,36 @@ withDefaults(
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.progress-wrap {
|
||||
display: grid;
|
||||
gap: 0.45rem;
|
||||
width: 100%;
|
||||
justify-items: center;
|
||||
}
|
||||
|
||||
.progress-track {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
border-radius: 999px;
|
||||
background: #e2e8f0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
background: linear-gradient(90deg, #0d9488, #14b8a6);
|
||||
transition: width 0.25s ease;
|
||||
}
|
||||
|
||||
.progress-pct {
|
||||
font-size: 1.15rem;
|
||||
font-weight: 800;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: #0f766e;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
|
||||
Reference in New Issue
Block a user