feat: 支持知识库 CSV 大文件导入

- 增加 CSV 流式解析、表格语义分块和分页预览

- 增加快照两阶段清理、失败重试和格式校验

- 补充批量入口、管理端交互和回归测试
This commit is contained in:
2026-08-07 13:11:59 +08:00
parent 13dec6c216
commit 0d14f1c165
32 changed files with 5672 additions and 156 deletions

View File

@@ -49,6 +49,8 @@ interface PreviewItem {
chunks?: ChunkItem[];
fileName: string;
normalizedContent?: string;
pageNo?: number;
pageSize?: number;
previewSessionId: string;
strategyCode?: string;
strategyLabel?: string;
@@ -97,6 +99,7 @@ const activeDocumentId = ref(props.documentId || '');
const previewError = ref('');
const previewLoading = ref(false);
const startLoading = ref(false);
const previewPageSize = 20;
let previewDebounceTimer: null | ReturnType<typeof setTimeout> = null;
let previewSequence = 0;
@@ -137,7 +140,9 @@ const fileExt = computed(
const isPptx = computed(() => fileExt.value === 'pptx');
const isXlsx = computed(() => fileExt.value === 'xlsx');
const showStrategySelector = computed(() => !isPptx.value && !isXlsx.value);
const isCsv = computed(() => fileExt.value === 'csv');
const isTabular = computed(() => isXlsx.value || isCsv.value);
const showStrategySelector = computed(() => !isPptx.value && !isTabular.value);
const mdLevels = [1, 2, 3, 4, 5, 6];
@@ -169,10 +174,10 @@ const buildStrategyConfig = () => {
strategyCode: 'OFFICE_PPTX_PAGE',
};
}
if (isXlsx.value) {
if (isTabular.value) {
return {
rowsPerChunk: formState.rowsPerChunk,
strategyCode: 'OFFICE_XLSX_ROW_WINDOW',
strategyCode: isCsv.value ? 'TABLE_ROW' : 'OFFICE_XLSX_ROW_WINDOW',
};
}
return {
@@ -233,6 +238,8 @@ const generatePreview = async () => {
},
],
knowledgeId: props.knowledgeId,
pageNo: 1,
pageSize: previewPageSize,
},
);
if (requestSequence !== previewSequence) {
@@ -261,6 +268,52 @@ const generatePreview = async () => {
}
};
const loadPreviewPage = async (pageNo: number) => {
if (
!activeDocumentId.value ||
!currentPreviewSessionId.value ||
previewLoading.value
) {
return;
}
const requestSequence = ++previewSequence;
previewLoading.value = true;
previewError.value = '';
try {
const res = await props.requestClient.post(
buildKnowledgePath(
props.endpointPrefix,
'/api/v1/document/import/task/preview',
),
{
documentId: activeDocumentId.value,
knowledgeId: props.knowledgeId,
pageNo,
pageSize: previewPageSize,
previewSessionId: currentPreviewSessionId.value,
},
);
if (requestSequence !== previewSequence) {
return;
}
previewItems.value = normalizePreviewItems(
(res.data?.items || []) as PreviewItem[],
);
} catch (error: any) {
if (requestSequence !== previewSequence) {
return;
}
const message =
error?.message || $t('documentCollection.importDoc.previewRequestFailed');
previewError.value = message;
ElMessage.error(message);
} finally {
if (requestSequence === previewSequence) {
previewLoading.value = false;
}
}
};
const schedulePreviewGeneration = () => {
if (!activeDocumentId.value) {
return;
@@ -321,8 +374,10 @@ watch(
if (isPptx.value) {
formState.strategyCode = 'OFFICE_PPTX_PAGE';
}
if (isXlsx.value) {
formState.strategyCode = 'OFFICE_XLSX_ROW_WINDOW';
if (isTabular.value) {
formState.strategyCode = isCsv.value
? 'TABLE_ROW'
: 'OFFICE_XLSX_ROW_WINDOW';
formState.rowsPerChunk = 10;
}
resetPreviewState();
@@ -365,7 +420,7 @@ watch(
</ElFormItem>
<ElFormItem
v-if="isXlsx"
v-if="isTabular"
label="每多少行分一块"
class="workbench__form-full"
>
@@ -450,6 +505,7 @@ watch(
<SplitterDocPreview
:loading="previewLoading"
:preview-items="previewItems"
@page-change="loadPreviewPage"
@preview-session-change="handlePreviewSessionChange"
/>
</section>