Import large activity Excel in the browser via JSON chunks to avoid 502.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3,10 +3,12 @@ import { computed, reactive, ref } from 'vue';
|
||||
import axios from 'axios';
|
||||
import { api } from '@/lib/api';
|
||||
import type {
|
||||
ExcelFieldOption,
|
||||
ExcelImportPrepareStatus,
|
||||
ExcelPreviewResult,
|
||||
ImportContactsResult,
|
||||
} from '@mento/shared';
|
||||
import { previewExcelClient, readMappedRowsClient } from '@/lib/excel-client';
|
||||
import { UiBusyOverlay, UiButton, UiModal, UiSelect } from '@/components/ui';
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -14,9 +16,25 @@ const props = defineProps<{
|
||||
importPath: string;
|
||||
/** اگر باشد: ابتدا فقط فایل خوانده میشود، بعد تکهتکه وارد میشود (برای فایلهای خیلی بزرگ) */
|
||||
preparePath?: string;
|
||||
/**
|
||||
* ایمپورت مرورگرگرا: اکسل در مرورگر خوانده میشود و فقط JSON کوچک به API میرود.
|
||||
* برای فایلهای دهها هزار ردیفی (جلوگیری از ۵۰۲ nginx).
|
||||
*/
|
||||
rowsImportPath?: string;
|
||||
clientAliases?: Record<string, string>;
|
||||
clientFields?: ExcelFieldOption[];
|
||||
disabled?: boolean;
|
||||
}>();
|
||||
|
||||
const useClientImport = computed(
|
||||
() =>
|
||||
Boolean(
|
||||
props.rowsImportPath &&
|
||||
props.clientAliases &&
|
||||
props.clientFields?.length,
|
||||
),
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
start: [];
|
||||
done: [result: ImportContactsResult];
|
||||
@@ -182,7 +200,62 @@ async function waitPrepareReady(sessionId: string) {
|
||||
throw new Error('آمادهسازی فایل بیش از حد طول کشید. دوباره تلاش کنید.');
|
||||
}
|
||||
|
||||
async function runClientRowsImport(file: File, map?: Record<string, string>) {
|
||||
if (!props.rowsImportPath || !props.clientAliases) {
|
||||
throw new Error('پیکربندی ایمپورت مرورگر ناقص است');
|
||||
}
|
||||
phase.value = 'import';
|
||||
progressLabel.value = 'در حال خواندن فایل در مرورگر…';
|
||||
progressPct.value = 0;
|
||||
|
||||
const rows = await readMappedRowsClient(file, props.clientAliases, map);
|
||||
const total = rows.length;
|
||||
if (total > 100_000) {
|
||||
return {
|
||||
created: 0,
|
||||
skipped: 0,
|
||||
errors: [{ row: 1, message: 'فایل بیش از ۱۰۰۰۰۰ ردیف دارد. فایل را تقسیم کنید.' }],
|
||||
total,
|
||||
processed: 0,
|
||||
hasMore: false,
|
||||
};
|
||||
}
|
||||
|
||||
updateImportProgress(0, total);
|
||||
let acc: ImportContactsResult = { created: 0, skipped: 0, errors: [] };
|
||||
let sessionId: string | undefined;
|
||||
const chunkSize = 60;
|
||||
|
||||
for (let offset = 0; offset < total; offset += chunkSize) {
|
||||
const slice = rows.slice(offset, offset + chunkSize);
|
||||
const { data: part } = await api.post<ImportContactsResult>(
|
||||
props.rowsImportPath,
|
||||
{
|
||||
rows: slice,
|
||||
offset,
|
||||
total,
|
||||
sessionId,
|
||||
},
|
||||
{ timeout: 120_000 },
|
||||
);
|
||||
acc = mergeImportResults(acc, part);
|
||||
updateImportProgress(part.processed ?? offset + slice.length, total);
|
||||
if (part.errors?.some((e) => e.row === 1 && /حداکثر|بیش از/.test(e.message))) {
|
||||
break;
|
||||
}
|
||||
sessionId = part.sessionId;
|
||||
if (!part.hasMore) break;
|
||||
}
|
||||
|
||||
updateImportProgress(total, total);
|
||||
return acc;
|
||||
}
|
||||
|
||||
async function runChunkedImport(file: File, map?: Record<string, string>) {
|
||||
if (useClientImport.value) {
|
||||
return runClientRowsImport(file, map);
|
||||
}
|
||||
|
||||
phase.value = 'import';
|
||||
progressLabel.value = 'در حال ارسال فایل…';
|
||||
progressPct.value = 0;
|
||||
@@ -229,7 +302,6 @@ async function runChunkedImport(file: File, map?: Record<string, string>) {
|
||||
const processed = part.processed ?? 0;
|
||||
updateImportProgress(processed, total);
|
||||
|
||||
// خطای سراسری (مثلاً سقف ردیف) — ادامه نده
|
||||
if (
|
||||
part.errors?.some((e) => e.row === 1 && /بیش از .* ردیف/.test(e.message))
|
||||
) {
|
||||
@@ -293,12 +365,19 @@ async function onFile(ev: Event) {
|
||||
submitting.value = true;
|
||||
phase.value = 'preview';
|
||||
try {
|
||||
const form = new FormData();
|
||||
form.append('file', file);
|
||||
const { data } = await api.post<ExcelPreviewResult>(props.previewPath, form, {
|
||||
headers: { 'Content-Type': undefined },
|
||||
timeout: 5 * 60 * 1000,
|
||||
});
|
||||
let data: ExcelPreviewResult;
|
||||
if (useClientImport.value && props.clientAliases && props.clientFields) {
|
||||
progressLabel.value = 'در حال خواندن فایل در مرورگر…';
|
||||
data = await previewExcelClient(file, props.clientAliases, props.clientFields);
|
||||
} else {
|
||||
const form = new FormData();
|
||||
form.append('file', file);
|
||||
const res = await api.post<ExcelPreviewResult>(props.previewPath, form, {
|
||||
headers: { 'Content-Type': undefined },
|
||||
timeout: 5 * 60 * 1000,
|
||||
});
|
||||
data = res.data;
|
||||
}
|
||||
if (!data.needsMapping) {
|
||||
try {
|
||||
emit('done', await runChunkedImport(file));
|
||||
|
||||
Reference in New Issue
Block a user