feat: 增强知识库分块策略流程
- 增加导入分析预览提交与预览态缓存键 - 支持知识库分块策略配置与分块预览 - 重构知识库导入与确认导入前端流程
This commit is contained in:
@@ -1,189 +1,215 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import { $t } from '@easyflow/locales';
|
||||
|
||||
import { Back } from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElButton,
|
||||
ElMessage,
|
||||
ElPagination,
|
||||
ElStep,
|
||||
ElSteps,
|
||||
} from 'element-plus';
|
||||
import { ElButton, ElMessage, ElStep, ElSteps } from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import ComfirmImportDocument from '#/views/ai/documentCollection/ComfirmImportDocument.vue';
|
||||
import ImportKnowledgeFileContainer from '#/views/ai/documentCollection/ImportKnowledgeFileContainer.vue';
|
||||
import SegmenterDoc from '#/views/ai/documentCollection/SegmenterDoc.vue';
|
||||
import SplitterDocPreview from '#/views/ai/documentCollection/SplitterDocPreview.vue';
|
||||
|
||||
interface UploadFileItem {
|
||||
fileName: string;
|
||||
filePath: string;
|
||||
}
|
||||
|
||||
interface AnalyzeItem {
|
||||
fileName: string;
|
||||
filePath: string;
|
||||
strategyConfig: Record<string, any>;
|
||||
}
|
||||
|
||||
interface PreviewItem {
|
||||
fileName: string;
|
||||
previewSessionId: string;
|
||||
totalChunks?: number;
|
||||
}
|
||||
|
||||
const emits = defineEmits(['importBack']);
|
||||
const back = () => {
|
||||
emits('importBack');
|
||||
};
|
||||
const files = ref([]);
|
||||
const splitterParams = ref({});
|
||||
const route = useRoute();
|
||||
const knowledgeId = computed(() => (route.query.id as string) || '');
|
||||
|
||||
const fileUploadRef = ref<InstanceType<typeof ImportKnowledgeFileContainer>>();
|
||||
const segmenterDocRef = ref<InstanceType<typeof SegmenterDoc>>();
|
||||
|
||||
const activeStep = ref(0);
|
||||
const fileUploadRef = ref();
|
||||
const confirmImportRef = ref();
|
||||
const segmenterDocRef = ref();
|
||||
const pagination = ref({
|
||||
pageSize: 10,
|
||||
currentPage: 1,
|
||||
total: 0,
|
||||
});
|
||||
const goToNextStep = () => {
|
||||
const files = ref<UploadFileItem[]>([]);
|
||||
const analysisItems = ref<AnalyzeItem[]>([]);
|
||||
const previewItems = ref<PreviewItem[]>([]);
|
||||
const commitResults = ref<any[]>([]);
|
||||
|
||||
const analyzing = ref(false);
|
||||
const previewing = ref(false);
|
||||
const committing = ref(false);
|
||||
|
||||
const canGoPrevious = computed(() => activeStep.value > 0 && !committing.value);
|
||||
|
||||
function back() {
|
||||
emits('importBack');
|
||||
}
|
||||
|
||||
function getUploadedFiles() {
|
||||
return fileUploadRef.value?.getFilesData?.() || [];
|
||||
}
|
||||
|
||||
async function goToNextStep() {
|
||||
if (activeStep.value === 0) {
|
||||
if (fileUploadRef.value.getFilesData().length === 0) {
|
||||
const currentFiles = getUploadedFiles();
|
||||
if (currentFiles.length === 0) {
|
||||
ElMessage.error($t('message.uploadFileFirst'));
|
||||
return;
|
||||
}
|
||||
files.value = fileUploadRef.value.getFilesData();
|
||||
files.value = currentFiles;
|
||||
await runAnalyze();
|
||||
activeStep.value = 1;
|
||||
return;
|
||||
}
|
||||
if (activeStep.value === 1 && segmenterDocRef.value) {
|
||||
splitterParams.value = segmenterDocRef.value.getSplitterFormValues();
|
||||
|
||||
if (activeStep.value === 1) {
|
||||
await runPreview();
|
||||
activeStep.value = 2;
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeStep.value === 2) {
|
||||
activeStep.value = 3;
|
||||
}
|
||||
}
|
||||
|
||||
function goToPreviousStep() {
|
||||
if (!canGoPrevious.value) {
|
||||
return;
|
||||
}
|
||||
activeStep.value += 1;
|
||||
};
|
||||
const goToPreviousStep = () => {
|
||||
activeStep.value -= 1;
|
||||
};
|
||||
const handleSizeChange = (val: number) => {
|
||||
pagination.value.pageSize = val;
|
||||
};
|
||||
const handleCurrentChange = (val: number) => {
|
||||
pagination.value.currentPage = val;
|
||||
};
|
||||
const handleTotalUpdate = (newTotal: number) => {
|
||||
pagination.value.total = newTotal; // 同步到父组件的 pagination.total
|
||||
};
|
||||
const loadingSave = ref(false);
|
||||
const confirmImport = () => {
|
||||
loadingSave.value = true;
|
||||
// 确认导入
|
||||
confirmImportRef.value.handleSave();
|
||||
};
|
||||
const finishImport = () => {
|
||||
loadingSave.value = false;
|
||||
ElMessage.success($t('documentCollection.splitterDoc.importSuccess'));
|
||||
emits('importBack');
|
||||
};
|
||||
}
|
||||
|
||||
async function runAnalyze() {
|
||||
analyzing.value = true;
|
||||
try {
|
||||
const res = await api.post('/api/v1/document/import/analyze', {
|
||||
files: files.value.map((item) => ({
|
||||
fileName: item.fileName,
|
||||
filePath: item.filePath,
|
||||
})),
|
||||
knowledgeId: knowledgeId.value,
|
||||
});
|
||||
analysisItems.value = res.data?.items || [];
|
||||
} finally {
|
||||
analyzing.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function runPreview() {
|
||||
const previewRequestItems =
|
||||
segmenterDocRef.value?.getPreviewRequestItems?.() || [];
|
||||
if (previewRequestItems.length === 0) {
|
||||
ElMessage.error($t('documentCollection.importDoc.previewEmpty'));
|
||||
return;
|
||||
}
|
||||
previewing.value = true;
|
||||
try {
|
||||
const res = await api.post('/api/v1/document/import/preview', {
|
||||
files: previewRequestItems,
|
||||
knowledgeId: knowledgeId.value,
|
||||
});
|
||||
previewItems.value = res.data?.items || [];
|
||||
commitResults.value = [];
|
||||
} finally {
|
||||
previewing.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmImport() {
|
||||
if (previewItems.value.length === 0) {
|
||||
ElMessage.error($t('documentCollection.importDoc.previewEmpty'));
|
||||
return;
|
||||
}
|
||||
committing.value = true;
|
||||
try {
|
||||
const res = await api.post('/api/v1/document/import/commit', {
|
||||
knowledgeId: knowledgeId.value,
|
||||
previewSessionIds: previewItems.value.map(
|
||||
(item) => item.previewSessionId,
|
||||
),
|
||||
});
|
||||
commitResults.value = res.data?.results || [];
|
||||
if ((res.data?.errorCount || 0) === 0) {
|
||||
ElMessage.success($t('documentCollection.splitterDoc.importSuccess'));
|
||||
}
|
||||
} finally {
|
||||
committing.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="imp-doc-kno-container">
|
||||
<div class="imp-doc-header">
|
||||
<ElButton @click="back" :icon="Back">
|
||||
<ElButton :icon="Back" @click="back">
|
||||
{{ $t('button.back') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
|
||||
<div class="imp-doc-kno-content">
|
||||
<div class="rounded-lg bg-[var(--table-header-bg-color)] py-5">
|
||||
<div class="step-card">
|
||||
<ElSteps :active="activeStep" align-center>
|
||||
<ElStep>
|
||||
<template #icon>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="h-8 w-8 rounded-full bg-[var(--step-item-bg)]">
|
||||
<span class="text-accent-foreground text-sm/8">1</span>
|
||||
</div>
|
||||
<span class="text-base">{{
|
||||
$t('documentCollection.importDoc.fileUpload')
|
||||
}}</span>
|
||||
</div>
|
||||
</template>
|
||||
</ElStep>
|
||||
<ElStep>
|
||||
<template #icon>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="h-8 w-8 rounded-full bg-[var(--step-item-bg)]">
|
||||
<span class="text-accent-foreground text-sm/8">2</span>
|
||||
</div>
|
||||
<span class="text-base">{{
|
||||
$t('documentCollection.importDoc.parameterSettings')
|
||||
}}</span>
|
||||
</div>
|
||||
</template>
|
||||
</ElStep>
|
||||
<ElStep>
|
||||
<template #icon>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="h-8 w-8 rounded-full bg-[var(--step-item-bg)]">
|
||||
<span class="text-accent-foreground text-sm/8">3</span>
|
||||
</div>
|
||||
<span class="text-base">{{
|
||||
$t('documentCollection.importDoc.segmentedPreview')
|
||||
}}</span>
|
||||
</div>
|
||||
</template>
|
||||
</ElStep>
|
||||
<ElStep>
|
||||
<template #icon>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="h-8 w-8 rounded-full bg-[var(--step-item-bg)]">
|
||||
<span class="text-accent-foreground text-sm/8">4</span>
|
||||
</div>
|
||||
<span class="text-base">{{
|
||||
$t('documentCollection.importDoc.confirmImport')
|
||||
}}</span>
|
||||
</div>
|
||||
</template>
|
||||
</ElStep>
|
||||
<ElStep :title="$t('documentCollection.importDoc.fileUpload')" />
|
||||
<ElStep
|
||||
:title="$t('documentCollection.importDoc.strategyAnalysis')"
|
||||
/>
|
||||
<ElStep
|
||||
:title="$t('documentCollection.importDoc.segmentedPreview')"
|
||||
/>
|
||||
<ElStep :title="$t('documentCollection.importDoc.confirmImport')" />
|
||||
</ElSteps>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 20px">
|
||||
<!-- 文件上传导入-->
|
||||
<div class="knw-file-upload" v-if="activeStep === 0">
|
||||
<ImportKnowledgeFileContainer ref="fileUploadRef" />
|
||||
</div>
|
||||
<!-- 分割参数设置-->
|
||||
<div class="knw-file-splitter" v-if="activeStep === 1">
|
||||
<SegmenterDoc ref="segmenterDocRef" />
|
||||
</div>
|
||||
<!-- 分割预览-->
|
||||
<div class="knw-file-preview" v-if="activeStep === 2">
|
||||
<SplitterDocPreview
|
||||
:flies-list="files"
|
||||
:splitter-params="splitterParams"
|
||||
:page-number="pagination.currentPage"
|
||||
:page-size="pagination.pageSize"
|
||||
@update-total="handleTotalUpdate"
|
||||
/>
|
||||
</div>
|
||||
<!-- 确认导入-->
|
||||
<div class="knw-file-confirm" v-if="activeStep === 3">
|
||||
<ComfirmImportDocument
|
||||
:splitter-params="splitterParams"
|
||||
:files-list="files"
|
||||
ref="confirmImportRef"
|
||||
@loading-finish="finishImport"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="height: 40px"></div>
|
||||
<div class="imp-doc-footer">
|
||||
<div v-if="activeStep === 2" class="imp-doc-page-container">
|
||||
<ElPagination
|
||||
:page-sizes="[10, 20]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="pagination.total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
<div class="step-body">
|
||||
<ImportKnowledgeFileContainer
|
||||
v-if="activeStep === 0"
|
||||
ref="fileUploadRef"
|
||||
/>
|
||||
<SegmenterDoc
|
||||
v-else-if="activeStep === 1"
|
||||
ref="segmenterDocRef"
|
||||
:analysis-items="analysisItems"
|
||||
/>
|
||||
<SplitterDocPreview
|
||||
v-else-if="activeStep === 2"
|
||||
:preview-items="previewItems"
|
||||
/>
|
||||
<ComfirmImportDocument
|
||||
v-else
|
||||
:preview-items="previewItems"
|
||||
:commit-results="commitResults"
|
||||
:loading="committing"
|
||||
/>
|
||||
</div>
|
||||
<ElButton @click="goToPreviousStep" type="primary" v-if="activeStep >= 1">
|
||||
</div>
|
||||
|
||||
<div class="imp-doc-footer">
|
||||
<ElButton v-if="canGoPrevious" @click="goToPreviousStep">
|
||||
{{ $t('button.previousStep') }}
|
||||
</ElButton>
|
||||
<ElButton @click="goToNextStep" type="primary" v-if="activeStep < 3">
|
||||
<ElButton
|
||||
v-if="activeStep < 3"
|
||||
type="primary"
|
||||
:loading="analyzing || previewing"
|
||||
@click="goToNextStep"
|
||||
>
|
||||
{{ $t('button.nextStep') }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
@click="confirmImport"
|
||||
v-else
|
||||
type="primary"
|
||||
v-if="activeStep === 3"
|
||||
:loading="loadingSave"
|
||||
:disabled="loadingSave"
|
||||
:loading="committing"
|
||||
:disabled="committing"
|
||||
@click="confirmImport"
|
||||
>
|
||||
{{ $t('button.startImport') }}
|
||||
</ElButton>
|
||||
@@ -194,60 +220,41 @@ const finishImport = () => {
|
||||
<style scoped>
|
||||
.imp-doc-kno-container {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
background-color: var(--el-bg-color);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
padding: 24px;
|
||||
border-radius: 16px;
|
||||
background: var(--el-bg-color);
|
||||
}
|
||||
|
||||
.imp-doc-kno-content {
|
||||
flex: 1;
|
||||
padding-top: 20px;
|
||||
overflow: auto;
|
||||
}
|
||||
.imp-doc-footer {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
display: flex;
|
||||
height: 40px;
|
||||
background-color: var(--el-bg-color);
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.knw-file-preview {
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
padding-top: 16px;
|
||||
overflow: auto;
|
||||
}
|
||||
.imp-doc-page-container {
|
||||
margin-right: 12px;
|
||||
}
|
||||
.knw-file-confirm {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-step__head) {
|
||||
--step-item-bg: rgba(0, 0, 0, 0.06);
|
||||
--step-item-solid-bg: rgba(0, 0, 0, 0.15);
|
||||
--accent-foreground: rgba(0, 0, 0, 0.45);
|
||||
.step-card {
|
||||
padding: 20px 24px;
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
border-radius: 16px;
|
||||
background: var(--el-fill-color-blank);
|
||||
}
|
||||
:deep(.el-step__head:where(.dark, .dark *)) {
|
||||
--step-item-bg: var(--el-text-color-placeholder);
|
||||
--step-item-solid-bg: var(--el-text-color-placeholder);
|
||||
--accent-foreground: var(--primary-foreground);
|
||||
|
||||
.step-body {
|
||||
flex: 1;
|
||||
padding-bottom: 72px;
|
||||
}
|
||||
:deep(.el-step__head.is-finish) {
|
||||
--step-item-bg: hsl(var(--primary));
|
||||
--step-item-solid-bg: hsl(var(--primary));
|
||||
--accent-foreground: var(--primary-foreground);
|
||||
}
|
||||
:deep(.el-step__icon.is-icon) {
|
||||
width: 120px;
|
||||
background-color: var(--table-header-bg-color);
|
||||
}
|
||||
:deep(.el-step__line) {
|
||||
background-color: var(--step-item-solid-bg);
|
||||
|
||||
.imp-doc-footer {
|
||||
position: absolute;
|
||||
right: 24px;
|
||||
bottom: 24px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user