Fixing the error when importing a large Excel file
This commit is contained in:
@@ -24,6 +24,7 @@ const preview = ref<ExcelPreviewResult | null>(null);
|
||||
const mapping = reactive<Record<string, string>>({});
|
||||
const submitting = ref(false);
|
||||
const phase = ref<'idle' | 'preview' | 'import'>('idle');
|
||||
const progressLabel = ref('');
|
||||
|
||||
const requiredKeys = computed(
|
||||
() => preview.value?.fields.filter((f) => f.required).map((f) => f.key) ?? [],
|
||||
@@ -34,9 +35,12 @@ const mappedFields = computed(() => new Set(Object.values(mapping).filter(Boolea
|
||||
const canImport = computed(() => requiredKeys.value.every((key) => mappedFields.value.has(key)));
|
||||
|
||||
const busyOpen = computed(() => submitting.value && phase.value !== 'idle');
|
||||
const busyMessage = computed(() =>
|
||||
phase.value === 'import' ? 'در حال ورود اطلاعات از اکسل…' : 'در حال خواندن و بررسی فایل اکسل…',
|
||||
);
|
||||
const busyMessage = computed(() => {
|
||||
if (phase.value === 'import') {
|
||||
return progressLabel.value || 'در حال ورود اطلاعات از اکسل…';
|
||||
}
|
||||
return 'در حال خواندن و بررسی فایل اکسل…';
|
||||
});
|
||||
|
||||
function pick() {
|
||||
if (props.disabled || submitting.value) return;
|
||||
@@ -48,6 +52,7 @@ function reset() {
|
||||
preview.value = null;
|
||||
submitting.value = false;
|
||||
phase.value = 'idle';
|
||||
progressLabel.value = '';
|
||||
for (const key of Object.keys(mapping)) delete mapping[key];
|
||||
}
|
||||
|
||||
@@ -66,21 +71,76 @@ function setColumnField(header: string, field: string) {
|
||||
mapping[header] = field;
|
||||
}
|
||||
|
||||
async function postImport(file: File, map?: Record<string, string>) {
|
||||
phase.value = 'import';
|
||||
function mergeImportResults(
|
||||
acc: ImportContactsResult,
|
||||
part: ImportContactsResult,
|
||||
): ImportContactsResult {
|
||||
const errors = [...acc.errors, ...(part.errors ?? [])];
|
||||
return {
|
||||
created: acc.created + (part.created ?? 0),
|
||||
skipped: acc.skipped + (part.skipped ?? 0),
|
||||
errors: errors.slice(0, 200),
|
||||
boardId: part.boardId ?? acc.boardId,
|
||||
boards: part.boards?.length ? part.boards : acc.boards,
|
||||
sessionId: part.sessionId,
|
||||
total: part.total ?? acc.total,
|
||||
processed: part.processed ?? acc.processed,
|
||||
hasMore: part.hasMore,
|
||||
nextOffset: part.nextOffset,
|
||||
};
|
||||
}
|
||||
|
||||
async function postImportChunk(opts: {
|
||||
file?: File;
|
||||
map?: Record<string, string>;
|
||||
sessionId?: string;
|
||||
offset?: number;
|
||||
}) {
|
||||
const form = new FormData();
|
||||
form.append('file', file);
|
||||
if (map && Object.values(map).some(Boolean)) {
|
||||
form.append('mapping', JSON.stringify(map));
|
||||
if (opts.file) form.append('file', opts.file);
|
||||
if (opts.map && Object.values(opts.map).some(Boolean)) {
|
||||
form.append('mapping', JSON.stringify(opts.map));
|
||||
}
|
||||
const { data } = await api.post<ImportContactsResult | Record<string, unknown>>(
|
||||
props.importPath,
|
||||
form,
|
||||
{
|
||||
headers: { 'Content-Type': undefined },
|
||||
},
|
||||
);
|
||||
return data as ImportContactsResult;
|
||||
if (opts.sessionId) form.append('sessionId', opts.sessionId);
|
||||
if (opts.offset != null) form.append('offset', String(opts.offset));
|
||||
const { data } = await api.post<ImportContactsResult>(props.importPath, form, {
|
||||
headers: { 'Content-Type': undefined },
|
||||
timeout: 3 * 60 * 1000,
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
async function runChunkedImport(file: File, map?: Record<string, string>) {
|
||||
phase.value = 'import';
|
||||
progressLabel.value = 'در حال ورود اطلاعات از اکسل…';
|
||||
|
||||
let acc: ImportContactsResult = { created: 0, skipped: 0, errors: [] };
|
||||
let sessionId: string | undefined;
|
||||
let offset = 0;
|
||||
let guard = 0;
|
||||
|
||||
while (guard < 500) {
|
||||
guard += 1;
|
||||
const part = await postImportChunk({
|
||||
file: sessionId ? undefined : file,
|
||||
map: sessionId ? undefined : map,
|
||||
sessionId,
|
||||
offset: sessionId ? offset : undefined,
|
||||
});
|
||||
acc = mergeImportResults(acc, part);
|
||||
|
||||
const total = part.total ?? 0;
|
||||
const processed = part.processed ?? 0;
|
||||
if (total > 0) {
|
||||
progressLabel.value = `ورود چندمرحلهای: ${processed.toLocaleString('fa-IR')} از ${total.toLocaleString('fa-IR')} ردیف`;
|
||||
}
|
||||
|
||||
if (!part.hasMore || !part.sessionId) break;
|
||||
sessionId = part.sessionId;
|
||||
offset = part.nextOffset ?? processed;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}
|
||||
|
||||
function apiMessage(e: unknown, fallback: string) {
|
||||
@@ -106,9 +166,15 @@ async function onFile(ev: Event) {
|
||||
headers: { 'Content-Type': undefined },
|
||||
});
|
||||
if (!data.needsMapping) {
|
||||
emit('done', await postImport(file));
|
||||
reset();
|
||||
emit('settled');
|
||||
try {
|
||||
emit('done', await runChunkedImport(file));
|
||||
reset();
|
||||
emit('settled');
|
||||
} catch (impErr) {
|
||||
emit('fail', apiMessage(impErr, 'ایمپورت ناموفق بود'));
|
||||
reset();
|
||||
emit('settled');
|
||||
}
|
||||
return;
|
||||
}
|
||||
pending.value = file;
|
||||
@@ -133,13 +199,14 @@ async function confirmMapping() {
|
||||
submitting.value = true;
|
||||
phase.value = 'import';
|
||||
try {
|
||||
emit('done', await postImport(pending.value, { ...mapping }));
|
||||
emit('done', await runChunkedImport(pending.value, { ...mapping }));
|
||||
reset();
|
||||
emit('settled');
|
||||
} catch (e) {
|
||||
emit('fail', apiMessage(e, 'ایمپورت ناموفق بود'));
|
||||
phase.value = 'idle';
|
||||
submitting.value = false;
|
||||
progressLabel.value = '';
|
||||
emit('settled');
|
||||
}
|
||||
}
|
||||
@@ -165,6 +232,9 @@ async function confirmMapping() {
|
||||
<p class="lead">
|
||||
ستونهای این فایل با قالب سیستم یکی نیست. مشخص کنید هر ستون فایل در کدام فیلد ریخته شود.
|
||||
</p>
|
||||
<p v-if="preview?.largeFile" class="large-hint">
|
||||
فایل بزرگ است ({{ (preview.rowCount ?? 0).toLocaleString('fa-IR') }} ردیف) — ورود بهصورت چندمرحلهای انجام میشود.
|
||||
</p>
|
||||
<div class="rows">
|
||||
<div v-for="col in preview?.columns ?? []" :key="col.header" class="row">
|
||||
<div class="source">
|
||||
@@ -209,6 +279,16 @@ async function confirmMapping() {
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.large-hint {
|
||||
margin: -0.35rem 0 1rem;
|
||||
padding: 0.55rem 0.75rem;
|
||||
border-radius: 10px;
|
||||
background: color-mix(in srgb, var(--accent, #2563eb) 12%, transparent);
|
||||
color: var(--fg, inherit);
|
||||
font-size: 0.88rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.rows {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
|
||||
Reference in New Issue
Block a user