初始化

This commit is contained in:
2026-02-22 18:56:10 +08:00
commit 26677972a6
3112 changed files with 255972 additions and 0 deletions

View File

@@ -0,0 +1,159 @@
<script setup lang="ts">
import { ref } from 'vue';
import { $t } from '@easyflow/locales';
import { Delete, MoreFilled } from '@element-plus/icons-vue';
import {
ElButton,
ElDialog,
ElDropdown,
ElDropdownItem,
ElDropdownMenu,
ElForm,
ElFormItem,
ElInput,
ElMessage,
ElMessageBox,
ElTable,
ElTableColumn,
} from 'element-plus';
import { api } from '#/api/request';
import PageData from '#/components/page/PageData.vue';
const props = defineProps({
documentId: {
type: String,
default: '',
},
});
const dialogVisible = ref(false);
const pageDataRef = ref();
const handleEdit = (row: any) => {
form.value = { id: row.id, content: row.content };
openDialog();
};
const handleDelete = (row: any) => {
ElMessageBox.confirm($t('message.deleteAlert'), $t('message.noticeTitle'), {
confirmButtonText: $t('message.ok'),
cancelButtonText: $t('message.cancel'),
type: 'warning',
})
.then(() => {
btnLoading.value = true;
api
.post('/api/v1/documentChunk/removeChunk', { id: row.id })
.then((res: any) => {
btnLoading.value = false;
if (res.errorCode !== 0) {
ElMessage.error(res.message);
return;
}
ElMessage.success($t('message.deleteOkMessage'));
pageDataRef.value.setQuery(queryParams);
});
})
.catch(() => {});
};
const openDialog = () => {
dialogVisible.value = true;
};
const closeDialog = () => {
dialogVisible.value = false;
};
const queryParams = ref({
documentId: props.documentId,
sortKey: 'sorting',
sortType: 'asc',
});
const save = () => {
btnLoading.value = true;
api.post('/api/v1/documentChunk/update', form.value).then((res: any) => {
btnLoading.value = false;
if (res.errorCode !== 0) {
ElMessage.error(res.message);
return;
}
ElMessage.success($t('message.updateOkMessage'));
pageDataRef.value.setQuery(queryParams);
closeDialog();
});
};
const btnLoading = ref(false);
const basicFormRef = ref();
const form = ref({
id: '',
content: '',
});
</script>
<template>
<div>
<PageData
page-url="/api/v1/documentChunk/page"
ref="pageDataRef"
:page-size="10"
:extra-query-params="queryParams"
>
<template #default="{ pageList }">
<ElTable :data="pageList" style="width: 100%" size="large">
<ElTableColumn
prop="content"
:label="$t('documentCollection.content')"
min-width="240"
/>
<ElTableColumn :label="$t('common.handle')" width="100" align="right">
<template #default="{ row }">
<div class="flex items-center gap-3">
<ElButton link type="primary" @click="handleEdit(row)">
{{ $t('button.edit') }}
</ElButton>
<ElDropdown>
<ElButton link :icon="MoreFilled" />
<template #dropdown>
<ElDropdownMenu>
<ElDropdownItem @click="handleDelete(row)">
<ElButton link type="danger" :icon="Delete">
{{ $t('button.delete') }}
</ElButton>
</ElDropdownItem>
</ElDropdownMenu>
</template>
</ElDropdown>
</div>
</template>
</ElTableColumn>
</ElTable>
</template>
</PageData>
<ElDialog v-model="dialogVisible" :title="$t('button.edit')" width="50%">
<ElForm
ref="basicFormRef"
style="width: 100%; margin-top: 20px"
:model="form"
>
<ElFormItem>
<ElInput v-model="form.content" :rows="20" type="textarea" />
</ElFormItem>
</ElForm>
<template #footer>
<ElButton @click="closeDialog">
{{ $t('button.cancel') }}
</ElButton>
<ElButton
type="primary"
@click="save"
:loading="btnLoading"
:disabled="btnLoading"
>
{{ $t('button.save') }}
</ElButton>
</template>
</ElDialog>
</div>
</template>
<style scoped></style>

View File

@@ -0,0 +1,99 @@
<script setup lang="ts">
import { ref, watch } from 'vue';
import { useRoute } from 'vue-router';
import { $t } from '@easyflow/locales';
import { ElTable, ElTableColumn, ElTag } from 'element-plus';
import { api } from '#/api/request';
const props = defineProps({
filesList: {
default: () => [],
type: Array<any>,
},
splitterParams: {
default: () => {},
type: Object,
},
});
const emit = defineEmits(['loadingFinish']);
const route = useRoute();
const knowledgeIdRef = ref<string>((route.query.id as string) || '');
const localFilesList = ref<any[]>([]);
watch(
() => props.filesList,
(newVal) => {
localFilesList.value = [...newVal];
},
{ immediate: true },
);
defineExpose({
handleSave() {
localFilesList.value.forEach((file, index) => {
localFilesList.value[index].progressUpload = 'loading';
saveDoc(file.filePath, 'saveText', file.fileName, index);
});
},
});
function saveDoc(
filePath: string,
operation: string,
fileOriginName: string,
index: number,
) {
api
.post('/api/v1/document/saveText', {
filePath,
operation,
knowledgeId: knowledgeIdRef.value,
fileOriginName,
...props.splitterParams,
})
.then((res) => {
if (res.errorCode === 0) {
localFilesList.value[index].progressUpload = 'success';
emit('loadingFinish');
}
/* if (index === localFilesList.value.length - 1) {
emit('loadingFinish');
}*/
});
}
</script>
<template>
<div class="import-doc-file-list">
<ElTable :data="localFilesList" size="large" style="width: 100%">
<ElTableColumn
prop="fileName"
:label="$t('documentCollection.importDoc.fileName')"
width="250"
/>
<ElTableColumn
prop="progressUpload"
:label="$t('documentCollection.splitterDoc.uploadStatus')"
>
<template #default="{ row }">
<ElTag type="success" v-if="row.progressUpload === 'success'">
{{ $t('documentCollection.splitterDoc.completed') }}
</ElTag>
<ElTag type="primary" v-else>
{{ $t('documentCollection.splitterDoc.pendingUpload') }}
</ElTag>
</template>
</ElTableColumn>
</ElTable>
</div>
</template>
<style scoped>
.import-doc-file-list {
width: 100%;
}
</style>

View File

@@ -0,0 +1,258 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { $t } from '@easyflow/locales';
import { ArrowLeft, Plus } from '@element-plus/icons-vue';
import { ElIcon, ElImage } from 'element-plus';
import { api } from '#/api/request';
import bookIcon from '#/assets/ai/knowledge/book.svg';
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
import PageSide from '#/components/page/PageSide.vue';
import ChunkDocumentTable from '#/views/ai/documentCollection/ChunkDocumentTable.vue';
import DocumentCollectionDataConfig from '#/views/ai/documentCollection/DocumentCollectionDataConfig.vue';
import DocumentTable from '#/views/ai/documentCollection/DocumentTable.vue';
import ImportKnowledgeDocFile from '#/views/ai/documentCollection/ImportKnowledgeDocFile.vue';
import KnowledgeSearch from '#/views/ai/documentCollection/KnowledgeSearch.vue';
import KnowledgeSearchConfig from '#/views/ai/documentCollection/KnowledgeSearchConfig.vue';
const route = useRoute();
const router = useRouter();
const knowledgeId = ref<string>((route.query.id as string) || '');
const activeMenu = ref<string>((route.query.activeMenu as string) || '');
const knowledgeInfo = ref<any>({});
const getKnowledge = () => {
api
.get('/api/v1/documentCollection/detail', {
params: { id: knowledgeId.value },
})
.then((res) => {
if (res.errorCode === 0) {
knowledgeInfo.value = res.data;
}
});
};
onMounted(() => {
if (activeMenu.value) {
defaultSelectedMenu.value = activeMenu.value;
}
getKnowledge();
});
const back = () => {
router.push({ path: '/ai/documentCollection' });
};
const categoryData = [
{ key: 'documentList', name: $t('documentCollection.documentList') },
{ key: 'knowledgeSearch', name: $t('documentCollection.knowledgeRetrieval') },
{ key: 'config', name: $t('documentCollection.config') },
];
const headerButtons = [
{
key: 'importFile',
text: $t('button.importFile'),
icon: Plus,
type: 'primary',
data: { action: 'importFile' },
},
];
const isImportFileVisible = ref(false);
const selectedCategory = ref('documentList');
const documentTableRef = ref();
const handleSearch = (searchParams: string) => {
documentTableRef.value.search(searchParams);
};
const handleButtonClick = (event: any) => {
// 根据按钮 key 执行不同操作
switch (event.key) {
case 'back': {
router.push({ path: '/ai/knowledge' });
break;
}
case 'importFile': {
isImportFileVisible.value = true;
break;
}
}
};
const handleCategoryClick = (category: any) => {
selectedCategory.value = category.key;
viewDocVisible.value = false;
};
const viewDocVisible = ref(false);
const documentId = ref('');
// 子组件传递事件,显示查看文档详情
const viewDoc = (docId: string) => {
viewDocVisible.value = true;
documentId.value = docId;
};
const backDoc = () => {
isImportFileVisible.value = false;
};
const defaultSelectedMenu = ref('documentList');
</script>
<template>
<div class="document-container">
<div v-if="!isImportFileVisible" class="doc-header-container">
<div class="doc-knowledge-container">
<div @click="back()" style="cursor: pointer">
<ElIcon><ArrowLeft /></ElIcon>
</div>
<div>
<ElImage :src="bookIcon" style="width: 36px; height: 36px" />
</div>
<div class="knowledge-info-container">
<div class="title">{{ knowledgeInfo.title || '' }}</div>
<div class="description">
{{ knowledgeInfo.description || '' }}
</div>
</div>
</div>
<div class="doc-content">
<div>
<PageSide
label-key="name"
value-key="key"
:menus="categoryData"
:default-selected="defaultSelectedMenu"
@change="handleCategoryClick"
/>
</div>
<div
class="doc-table-content menu-container border border-[var(--el-border-color)]"
>
<div v-if="selectedCategory === 'documentList'" class="doc-table">
<div class="doc-header" v-if="!viewDocVisible">
<HeaderSearch
:buttons="headerButtons"
@search="handleSearch"
@button-click="handleButtonClick"
/>
</div>
<DocumentTable
ref="documentTableRef"
:knowledge-id="knowledgeId"
@view-doc="viewDoc"
v-if="!viewDocVisible"
/>
<ChunkDocumentTable
v-else
:document-id="documentId"
:default-summary-prompt="knowledgeInfo.summaryPrompt"
/>
</div>
<!--知识检索-->
<div
v-if="selectedCategory === 'knowledgeSearch'"
class="doc-search-container"
>
<KnowledgeSearchConfig :document-collection-id="knowledgeId" />
<KnowledgeSearch :knowledge-id="knowledgeId" />
</div>
<!--配置-->
<div v-if="selectedCategory === 'config'">
<DocumentCollectionDataConfig
:detail-data="knowledgeInfo"
@reload="getKnowledge"
/>
</div>
</div>
</div>
</div>
<div v-else class="doc-imp-container">
<ImportKnowledgeDocFile @import-back="backDoc" />
</div>
</div>
</template>
<style scoped>
.document-container {
width: 100%;
display: flex;
height: 100%;
padding: 24px 24px 30px 24px;
}
.doc-container {
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
box-sizing: border-box;
}
.doc-table-content {
border-radius: 8px;
width: 100%;
box-sizing: border-box;
padding: 20px 14px 0 14px;
background-color: var(--el-bg-color);
flex: 1;
}
.doc-header {
width: 100%;
margin: 0 auto;
padding-bottom: 21px;
}
.doc-content {
display: flex;
flex-direction: row;
height: 100%;
width: 100%;
gap: 12px;
}
.doc-table {
background-color: var(--el-bg-color);
}
.doc-imp-container {
flex: 1;
width: 100%;
box-sizing: border-box;
}
.doc-header-container {
display: flex;
flex-direction: column;
width: 100%;
}
.doc-knowledge-container {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 20px;
gap: 8px;
}
.knowledge-info-container {
display: flex;
flex-direction: column;
gap: 4px;
}
.title {
font-weight: 500;
font-size: 16px;
line-height: 24px;
text-align: left;
font-style: normal;
text-transform: none;
}
.description {
font-weight: 400;
font-size: 14px;
color: #75808d;
line-height: 22px;
text-align: left;
font-style: normal;
text-transform: none;
}
.doc-search-container {
width: 100%;
height: 100%;
display: flex;
}
.menu-container {
flex: 1;
}
</style>

View File

@@ -0,0 +1,371 @@
<script setup lang="ts">
import type { FormInstance } from 'element-plus';
import type { ActionButton } from '#/components/page/CardList.vue';
import { computed, onMounted, ref } from 'vue';
import { useRouter } from 'vue-router';
import { $t } from '@easyflow/locales';
import { Delete, Edit, Notebook, Plus, Search } from '@element-plus/icons-vue';
import {
ElButton,
ElDialog,
ElForm,
ElFormItem,
ElInput,
ElInputNumber,
ElMessage,
ElMessageBox,
} from 'element-plus';
import { tryit } from 'radash';
import { api } from '#/api/request';
import defaultIcon from '#/assets/ai/knowledge/book.svg';
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
import CardPage from '#/components/page/CardList.vue';
import PageData from '#/components/page/PageData.vue';
import PageSide from '#/components/page/PageSide.vue';
import DocumentCollectionModal from '#/views/ai/documentCollection/DocumentCollectionModal.vue';
const router = useRouter();
interface FieldDefinition {
// 字段名称
prop: string;
// 字段标签
label: string;
// 字段类型input, number, select, radio, checkbox, switch, date, datetime
type?: 'input' | 'number';
// 是否必填
required?: boolean;
// 占位符
placeholder?: string;
}
// 操作按钮配置
const actions: ActionButton[] = [
{
icon: Edit,
text: $t('button.edit'),
className: '',
permission: '/api/v1/documentCollection/save',
onClick(row) {
aiKnowledgeModalRef.value.openDialog(row);
},
},
{
icon: Notebook,
text: $t('documentCollection.actions.knowledge'),
className: '',
permission: '/api/v1/documentCollection/save',
onClick(row) {
router.push({
path: '/ai/documentCollection/document',
query: {
id: row.id,
pageKey: '/ai/documentCollection',
},
});
},
},
{
icon: Search,
text: $t('documentCollection.actions.retrieve'),
className: '',
permission: '',
onClick(row) {
router.push({
path: '/ai/documentCollection/document',
query: {
id: row.id,
pageKey: '/ai/documentCollection',
activeMenu: 'knowledgeSearch',
},
});
},
},
{
text: $t('button.delete'),
icon: Delete,
className: 'item-danger',
permission: '/api/v1/documentCollection/remove',
onClick(row) {
handleDelete(row);
},
},
];
onMounted(() => {
getCategoryList();
});
const handleDelete = (item: any) => {
ElMessageBox.confirm($t('message.deleteAlert'), $t('message.noticeTitle'), {
confirmButtonText: $t('message.ok'),
cancelButtonText: $t('message.cancel'),
type: 'warning',
})
.then(() => {
api
.post('/api/v1/documentCollection/remove', { id: item.id })
.then((res) => {
if (res.errorCode === 0) {
ElMessage.success($t('message.deleteOkMessage'));
pageDataRef.value.setQuery({});
}
});
})
.catch(() => {});
};
const pageDataRef = ref();
const aiKnowledgeModalRef = ref();
const headerButtons = [
{
key: 'add',
text: $t('documentCollection.actions.addKnowledge'),
icon: Plus,
type: 'primary',
data: { action: 'add' },
permission: '/api/v1/documentCollection/save',
},
];
const handleButtonClick = (event: any, _item: any) => {
switch (event.key) {
case 'add': {
aiKnowledgeModalRef.value.openDialog({});
break;
}
}
};
const fieldDefinitions = ref<FieldDefinition[]>([
{
prop: 'categoryName',
label: $t('aiWorkflowCategory.categoryName'),
type: 'input',
required: true,
placeholder: $t('aiWorkflowCategory.categoryName'),
},
{
prop: 'sortNo',
label: $t('aiWorkflowCategory.sortNo'),
type: 'number',
required: false,
placeholder: $t('aiWorkflowCategory.sortNo'),
},
]);
const formRules = computed(() => {
const rules: Record<string, any[]> = {};
fieldDefinitions.value.forEach((field) => {
const fieldRules = [];
if (field.required) {
fieldRules.push({
required: true,
message: `${$t('message.required')}`,
trigger: 'blur',
});
}
if (fieldRules.length > 0) {
rules[field.prop] = fieldRules;
}
});
return rules;
});
const handleSearch = (params: any) => {
pageDataRef.value.setQuery({ title: params, isQueryOr: true });
};
const formData = ref<any>({});
const dialogVisible = ref(false);
const formRef = ref<FormInstance>();
function showControlDialog(item: any) {
formRef.value?.resetFields();
formData.value = { ...item };
dialogVisible.value = true;
}
const categoryList = ref<any[]>([]);
const getCategoryList = async () => {
const [, res] = await tryit(api.get)(
'/api/v1/documentCollectionCategory/list',
{
params: { sortKey: 'sortNo', sortType: 'asc' },
},
);
if (res && res.errorCode === 0) {
categoryList.value = [
{
id: '',
categoryName: $t('common.allCategories'),
},
...res.data,
];
}
};
function removeCategory(row: any) {
ElMessageBox.confirm($t('message.deleteAlert'), $t('message.noticeTitle'), {
confirmButtonText: $t('message.ok'),
cancelButtonText: $t('message.cancel'),
type: 'warning',
beforeClose: (action, instance, done) => {
if (action === 'confirm') {
instance.confirmButtonLoading = true;
api
.post('/api/v1/documentCollectionCategory/remove', { id: row.id })
.then((res) => {
instance.confirmButtonLoading = false;
if (res.errorCode === 0) {
ElMessage.success(res.message);
done();
getCategoryList();
}
})
.catch(() => {
instance.confirmButtonLoading = false;
});
} else {
done();
}
},
}).catch(() => {});
}
const controlBtns = [
{
icon: Edit,
label: $t('button.edit'),
onClick(row: any) {
showControlDialog(row);
},
},
{
type: 'danger',
icon: Delete,
label: $t('button.delete'),
onClick(row: any) {
removeCategory(row);
},
},
];
const footerButton = {
icon: Plus,
label: $t('button.add'),
onClick() {
showControlDialog({});
},
};
const saveLoading = ref(false);
function handleSubmit() {
formRef.value?.validate((valid) => {
if (valid) {
saveLoading.value = true;
const url = formData.value.id
? '/api/v1/documentCollectionCategory/update'
: '/api/v1/documentCollectionCategory/save';
api.post(url, formData.value).then((res) => {
saveLoading.value = false;
if (res.errorCode === 0) {
getCategoryList();
ElMessage.success(res.message);
dialogVisible.value = false;
}
});
}
});
}
function changeCategory(category: any) {
pageDataRef.value.setQuery({ categoryId: category.id });
}
</script>
<template>
<div class="flex h-full flex-col gap-6 p-6">
<div class="knowledge-header">
<HeaderSearch
:buttons="headerButtons"
@search="handleSearch"
@button-click="handleButtonClick"
/>
</div>
<div class="flex max-h-[calc(100vh-191px)] flex-1 gap-6">
<PageSide
label-key="categoryName"
value-key="id"
:menus="categoryList"
:control-btns="controlBtns"
:footer-button="footerButton"
@change="changeCategory"
/>
<div class="h-full flex-1 overflow-auto">
<PageData
ref="pageDataRef"
page-url="/api/v1/documentCollection/page"
:page-size="12"
:page-sizes="[12, 24, 36, 48]"
:init-query-params="{ status: 1 }"
>
<template #default="{ pageList }">
<CardPage
:default-icon="defaultIcon"
title-key="title"
avatar-key="icon"
description-key="description"
:data="pageList"
:actions="actions"
/>
</template>
</PageData>
</div>
</div>
<ElDialog
v-model="dialogVisible"
:title="formData.id ? `${$t('button.edit')}` : `${$t('button.add')}`"
:close-on-click-modal="false"
>
<ElForm
ref="formRef"
:model="formData"
:rules="formRules"
label-width="120px"
>
<!-- 动态生成表单项 -->
<ElFormItem
v-for="field in fieldDefinitions"
:key="field.prop"
:label="field.label"
:prop="field.prop"
>
<ElInput
v-if="!field.type || field.type === 'input'"
v-model="formData[field.prop]"
:placeholder="field.placeholder"
/>
<ElInputNumber
v-else-if="field.type === 'number'"
v-model="formData[field.prop]"
:placeholder="field.placeholder"
style="width: 100%"
/>
</ElFormItem>
</ElForm>
<template #footer>
<ElButton @click="dialogVisible = false">
{{ $t('button.cancel') }}
</ElButton>
<ElButton type="primary" @click="handleSubmit" :loading="saveLoading">
{{ $t('button.confirm') }}
</ElButton>
</template>
</ElDialog>
<!-- 新增知识库模态框-->
<DocumentCollectionModal ref="aiKnowledgeModalRef" @reload="handleSearch" />
</div>
</template>
<style scoped>
h1 {
text-align: center;
margin-bottom: 30px;
color: #303133;
}
</style>

View File

@@ -0,0 +1,346 @@
<script setup lang="ts">
import type { FormInstance } from 'element-plus';
import { onMounted, ref, watch } from 'vue';
import { InfoFilled } from '@element-plus/icons-vue';
import {
ElButton,
ElForm,
ElFormItem,
ElIcon,
ElInput,
ElMessage,
ElOption,
ElSelect,
ElSwitch,
ElTooltip,
} from 'element-plus';
import { api } from '#/api/request';
import UploadAvatar from '#/components/upload/UploadAvatar.vue';
import { $t } from '#/locales';
const props = defineProps({
detailData: {
type: Object,
default: () => ({
id: '',
alias: '',
deptId: '',
icon: '',
title: '',
description: '',
slug: '',
vectorStoreEnable: false,
vectorStoreType: '',
vectorStoreCollection: '',
vectorStoreConfig: '',
vectorEmbedModelId: '',
options: {
canUpdateEmbeddingModel: true,
},
rerankModelId: '',
searchEngineEnable: false,
englishName: '',
}),
required: true,
},
});
const emit = defineEmits(['reload']);
const entity = ref<any>({ ...props.detailData });
watch(
() => props.detailData,
(newVal) => {
entity.value = { ...newVal };
},
{ immediate: true, deep: true },
);
const embeddingLlmList = ref<any>([]);
const rerankerLlmList = ref<any>([]);
const vecotrDatabaseList = ref<any>([
// { value: 'milvus', label: 'Milvus' },
{ value: 'redis', label: 'Redis' },
{ value: 'opensearch', label: 'OpenSearch' },
{ value: 'elasticsearch', label: 'ElasticSearch' },
{ value: 'aliyun', label: $t('documentCollection.alibabaCloud') },
{ value: 'qcloud', label: $t('documentCollection.tencentCloud') },
]);
const getEmbeddingLlmListData = async () => {
try {
const url = `/api/v1/model/list?modelType=embeddingModel`;
const res = await api.get(url, {});
if (res.errorCode === 0) {
embeddingLlmList.value = res.data;
}
} catch (error) {
ElMessage.error($t('message.apiError'));
console.error('获取嵌入模型列表失败:', error);
}
};
const getRerankerLlmListData = async () => {
try {
const res = await api.get('/api/v1/model/list?modelType=rerankModel');
rerankerLlmList.value = res.data;
} catch (error) {
ElMessage.error($t('message.apiError'));
console.error('获取重排模型列表失败:', error);
}
};
onMounted(async () => {
await getEmbeddingLlmListData();
await getRerankerLlmListData();
});
const saveForm = ref<FormInstance>();
const btnLoading = ref(false);
const rules = ref({
deptId: [
{ required: true, message: $t('message.required'), trigger: 'blur' },
],
englishName: [
{ required: true, message: $t('message.required'), trigger: 'blur' },
],
description: [
{ required: true, message: $t('message.required'), trigger: 'blur' },
],
title: [{ required: true, message: $t('message.required'), trigger: 'blur' }],
vectorStoreType: [
{ required: true, message: $t('message.required'), trigger: 'blur' },
],
vectorStoreCollection: [
{ required: true, message: $t('message.required'), trigger: 'blur' },
],
vectorStoreConfig: [
{ required: true, message: $t('message.required'), trigger: 'blur' },
],
vectorEmbedModelId: [
{ required: true, message: $t('message.required'), trigger: 'blur' },
],
});
async function save() {
try {
const valid = await saveForm.value?.validate();
if (!valid) return;
btnLoading.value = true;
const res = await api.post(
'/api/v1/documentCollection/update',
entity.value,
);
if (res.errorCode === 0) {
ElMessage.success($t('message.saveOkMessage'));
emit('reload');
}
} catch (error) {
ElMessage.error($t('message.saveFail'));
console.error('保存失败:', error);
} finally {
btnLoading.value = false;
}
}
</script>
<template>
<div class="document-config-container">
<ElForm
label-width="150px"
ref="saveForm"
:model="entity"
status-icon
:rules="rules"
>
<ElFormItem
prop="icon"
:label="$t('documentCollection.icon')"
style="display: flex; align-items: center"
>
<UploadAvatar v-model="entity.icon" />
</ElFormItem>
<ElFormItem prop="title" :label="$t('documentCollection.title')">
<ElInput
v-model.trim="entity.title"
:placeholder="$t('documentCollection.placeholder.title')"
/>
</ElFormItem>
<ElFormItem prop="alias" :label="$t('documentCollection.alias')">
<ElInput v-model.trim="entity.alias" />
</ElFormItem>
<ElFormItem
prop="englishName"
:label="$t('documentCollection.englishName')"
>
<ElInput v-model.trim="entity.englishName" />
</ElFormItem>
<ElFormItem
prop="description"
:label="$t('documentCollection.description')"
>
<ElInput
v-model.trim="entity.description"
:rows="4"
type="textarea"
:placeholder="$t('documentCollection.placeholder.description')"
/>
</ElFormItem>
<!-- <ElFormItem
prop="vectorStoreEnable"
:label="$t('documentCollection.vectorStoreEnable')"
>
<ElSwitch v-model="entity.vectorStoreEnable" />
</ElFormItem>-->
<ElFormItem
prop="vectorStoreType"
:label="$t('documentCollection.vectorStoreType')"
>
<ElSelect
v-model="entity.vectorStoreType"
:placeholder="$t('documentCollection.placeholder.vectorStoreType')"
>
<ElOption
v-for="item in vecotrDatabaseList"
:key="item.value"
:label="item.label"
:value="item.value || ''"
/>
</ElSelect>
</ElFormItem>
<ElFormItem
prop="vectorStoreCollection"
:label="$t('documentCollection.vectorStoreCollection')"
>
<ElInput
v-model.trim="entity.vectorStoreCollection"
:placeholder="
$t('documentCollection.placeholder.vectorStoreCollection')
"
/>
</ElFormItem>
<ElFormItem
prop="vectorStoreConfig"
:label="$t('documentCollection.vectorStoreConfig')"
>
<ElInput
v-model.trim="entity.vectorStoreConfig"
:rows="4"
type="textarea"
/>
</ElFormItem>
<ElFormItem prop="vectorEmbedModelId">
<template #label>
<span style="display: flex; align-items: center">
{{ $t('documentCollection.vectorEmbedLlmId') }}
<ElTooltip
:content="$t('documentCollection.vectorEmbedModelTips')"
placement="top"
effect="light"
>
<ElIcon
style="
margin-left: 4px;
color: #909399;
cursor: pointer;
font-size: 14px;
"
>
<InfoFilled />
</ElIcon>
</ElTooltip>
</span>
</template>
<ElSelect
v-model="entity.vectorEmbedModelId"
:disabled="!entity?.options?.canUpdateEmbeddingModel"
:placeholder="$t('documentCollection.placeholder.embedLlm')"
>
<ElOption
v-for="item in embeddingLlmList"
:key="item.id"
:label="item.title"
:value="item.id || ''"
/>
</ElSelect>
</ElFormItem>
<ElFormItem
prop="dimensionOfVectorModel"
:label="$t('documentCollection.dimensionOfVectorModel')"
>
<template #label>
<span style="display: flex; align-items: center">
{{ $t('documentCollection.dimensionOfVectorModel') }}
<ElTooltip
:content="$t('documentCollection.dimensionOfVectorModelTips')"
placement="top"
effect="light"
>
<ElIcon
style="
margin-left: 4px;
color: #909399;
cursor: pointer;
font-size: 14px;
"
>
<InfoFilled />
</ElIcon>
</ElTooltip>
</span>
</template>
<ElInput
:disabled="!entity?.options?.canUpdateEmbeddingModel"
v-model.trim="entity.dimensionOfVectorModel"
type="number"
/>
</ElFormItem>
<ElFormItem
prop="rerankModelId"
:label="$t('documentCollection.rerankLlmId')"
>
<ElSelect
v-model="entity.rerankModelId"
:placeholder="$t('documentCollection.placeholder.rerankLlm')"
>
<ElOption
v-for="item in rerankerLlmList"
:key="item.id"
:label="item.title"
:value="item.id || ''"
/>
</ElSelect>
</ElFormItem>
<ElFormItem
prop="searchEngineEnable"
:label="$t('documentCollection.searchEngineEnable')"
>
<ElSwitch v-model="entity.searchEngineEnable" />
</ElFormItem>
<ElFormItem style="margin-top: 20px; text-align: right">
<ElButton
type="primary"
@click="save"
:loading="btnLoading"
:disabled="btnLoading"
>
{{ $t('button.save') }}
</ElButton>
</ElFormItem>
</ElForm>
</div>
</template>
<style scoped>
.document-config-container {
height: 100%;
overflow: auto;
}
</style>

View File

@@ -0,0 +1,381 @@
<script setup lang="ts">
import type { FormInstance } from 'element-plus';
import { onMounted, ref } from 'vue';
import { InfoFilled } from '@element-plus/icons-vue';
import {
ElButton,
ElDialog,
ElForm,
ElFormItem,
ElIcon,
ElInput,
ElMessage,
ElOption,
ElSelect,
ElSwitch,
ElTooltip,
} from 'element-plus';
import { api } from '#/api/request';
import DictSelect from '#/components/dict/DictSelect.vue';
import UploadAvatar from '#/components/upload/UploadAvatar.vue';
import { $t } from '#/locales';
const emit = defineEmits(['reload']);
const embeddingLlmList = ref<any>([]);
const rerankerLlmList = ref<any>([]);
const getEmbeddingLlmListData = async () => {
try {
const url = `/api/v1/model/list?modelType=embeddingModel`;
const res = await api.get(url, {});
if (res.errorCode === 0) {
embeddingLlmList.value = res.data;
}
} catch (error) {
ElMessage.error($t('message.apiError'));
console.error('获取嵌入模型列表失败:', error);
}
};
const getRerankerLlmListData = async () => {
try {
const res = await api.get('/api/v1/model/list?modelType=rerankModel');
rerankerLlmList.value = res.data;
} catch (error) {
ElMessage.error($t('message.apiError'));
console.error('获取重排模型列表失败:', error);
}
};
onMounted(async () => {
await getEmbeddingLlmListData();
await getRerankerLlmListData();
});
const saveForm = ref<FormInstance>();
const dialogVisible = ref(false);
const isAdd = ref(true);
const vecotrDatabaseList = ref<any>([
{ value: 'milvus', label: 'Milvus' },
{ value: 'redis', label: 'Redis' },
{ value: 'opensearch', label: 'OpenSearch' },
{ value: 'elasticsearch', label: 'ElasticSearch' },
{ value: 'aliyun', label: $t('documentCollection.alibabaCloud') },
{ value: 'qcloud', label: $t('documentCollection.tencentCloud') },
]);
const defaultEntity = {
alias: '',
deptId: '',
icon: '',
title: '',
categoryId: '',
description: '',
slug: '',
vectorStoreEnable: false,
vectorStoreType: '',
vectorStoreCollection: '',
vectorStoreConfig: '',
vectorEmbedModelId: '',
dimensionOfVectorModel: undefined,
options: {
canUpdateEmbeddingModel: true,
},
rerankModelId: '',
searchEngineEnable: '',
englishName: '',
};
const entity = ref<any>({ ...defaultEntity });
const btnLoading = ref(false);
const rules = ref({
deptId: [
{ required: true, message: $t('message.required'), trigger: 'blur' },
],
englishName: [
{ required: true, message: $t('message.required'), trigger: 'blur' },
],
description: [
{ required: true, message: $t('message.required'), trigger: 'blur' },
],
title: [{ required: true, message: $t('message.required'), trigger: 'blur' }],
vectorStoreType: [
{ required: true, message: $t('message.required'), trigger: 'blur' },
],
vectorStoreCollection: [
{ required: true, message: $t('message.required'), trigger: 'blur' },
],
vectorStoreConfig: [
{ required: true, message: $t('message.required'), trigger: 'blur' },
],
vectorEmbedModelId: [
{ required: true, message: $t('message.required'), trigger: 'blur' },
],
});
function openDialog(row: any = {}) {
if (row.id) {
isAdd.value = false;
entity.value = {
...defaultEntity,
...row,
options: { ...defaultEntity.options, ...row.options },
};
} else {
isAdd.value = true;
entity.value = { ...defaultEntity };
}
dialogVisible.value = true;
}
async function save() {
try {
const valid = await saveForm.value?.validate();
if (!valid) return;
btnLoading.value = true;
const res = await api.post(
isAdd.value
? '/api/v1/documentCollection/save'
: '/api/v1/documentCollection/update',
entity.value,
);
if (res.errorCode === 0) {
ElMessage.success(res.message || $t('message.saveSuccess'));
emit('reload');
closeDialog();
} else {
ElMessage.error(res.message || $t('message.saveFail'));
}
} catch (error) {
ElMessage.error($t('message.saveFail'));
console.error('保存失败:', error);
} finally {
btnLoading.value = false;
}
}
function closeDialog() {
saveForm.value?.resetFields();
isAdd.value = true;
entity.value = { ...defaultEntity };
dialogVisible.value = false;
}
defineExpose({
openDialog,
});
</script>
<template>
<ElDialog
v-model="dialogVisible"
draggable
:title="isAdd ? $t('button.add') : $t('button.edit')"
:before-close="closeDialog"
:close-on-click-modal="false"
align-center
>
<ElForm
label-width="150px"
ref="saveForm"
:model="entity"
status-icon
:rules="rules"
>
<ElFormItem
prop="icon"
:label="$t('documentCollection.icon')"
style="display: flex; align-items: center"
>
<UploadAvatar v-model="entity.icon" />
</ElFormItem>
<ElFormItem prop="title" :label="$t('documentCollection.title')">
<ElInput
v-model.trim="entity.title"
:placeholder="$t('documentCollection.placeholder.title')"
/>
</ElFormItem>
<ElFormItem
prop="categoryId"
:label="$t('documentCollection.categoryId')"
>
<DictSelect
v-model="entity.categoryId"
dict-code="aiDocumentCollectionCategory"
/>
</ElFormItem>
<ElFormItem prop="alias" :label="$t('documentCollection.alias')">
<ElInput v-model.trim="entity.alias" />
</ElFormItem>
<ElFormItem
prop="englishName"
:label="$t('documentCollection.englishName')"
>
<ElInput v-model.trim="entity.englishName" />
</ElFormItem>
<ElFormItem
prop="description"
:label="$t('documentCollection.description')"
>
<ElInput
v-model.trim="entity.description"
:rows="4"
type="textarea"
:placeholder="$t('documentCollection.placeholder.description')"
/>
</ElFormItem>
<!-- <ElFormItem
prop="vectorStoreEnable"
:label="$t('documentCollection.vectorStoreEnable')"
>
<ElSwitch v-model="entity.vectorStoreEnable" />
</ElFormItem>-->
<ElFormItem
prop="vectorStoreType"
:label="$t('documentCollection.vectorStoreType')"
>
<ElSelect
v-model="entity.vectorStoreType"
:placeholder="$t('documentCollection.placeholder.vectorStoreType')"
>
<ElOption
v-for="item in vecotrDatabaseList"
:key="item.value"
:label="item.label"
:value="item.value || ''"
/>
</ElSelect>
</ElFormItem>
<ElFormItem
prop="vectorStoreCollection"
:label="$t('documentCollection.vectorStoreCollection')"
>
<ElInput
v-model.trim="entity.vectorStoreCollection"
:placeholder="
$t('documentCollection.placeholder.vectorStoreCollection')
"
/>
</ElFormItem>
<ElFormItem
prop="vectorStoreConfig"
:label="$t('documentCollection.vectorStoreConfig')"
>
<ElInput
v-model.trim="entity.vectorStoreConfig"
:rows="4"
type="textarea"
/>
</ElFormItem>
<ElFormItem prop="vectorEmbedModelId">
<template #label>
<span style="display: flex; align-items: center">
{{ $t('documentCollection.vectorEmbedLlmId') }}
<ElTooltip
:content="$t('documentCollection.vectorEmbedModelTips')"
placement="top"
effect="light"
>
<ElIcon
style="
margin-left: 4px;
color: #909399;
cursor: pointer;
font-size: 14px;
"
>
<InfoFilled />
</ElIcon>
</ElTooltip>
</span>
</template>
<ElSelect
v-model="entity.vectorEmbedModelId"
:disabled="!entity?.options?.canUpdateEmbeddingModel"
:placeholder="$t('documentCollection.placeholder.embedLlm')"
>
<ElOption
v-for="item in embeddingLlmList"
:key="item.id"
:label="item.title"
:value="item.id || ''"
/>
</ElSelect>
</ElFormItem>
<ElFormItem
prop="dimensionOfVectorModel"
:label="$t('documentCollection.dimensionOfVectorModel')"
>
<template #label>
<span style="display: flex; align-items: center">
{{ $t('documentCollection.dimensionOfVectorModel') }}
<ElTooltip
:content="$t('documentCollection.dimensionOfVectorModelTips')"
placement="top"
effect="light"
>
<ElIcon
style="
margin-left: 4px;
color: #909399;
cursor: pointer;
font-size: 14px;
"
>
<InfoFilled />
</ElIcon>
</ElTooltip>
</span>
</template>
<ElInput
:disabled="!entity?.options?.canUpdateEmbeddingModel"
v-model.trim="entity.dimensionOfVectorModel"
type="number"
/>
</ElFormItem>
<ElFormItem
prop="rerankModelId"
:label="$t('documentCollection.rerankLlmId')"
>
<ElSelect
v-model="entity.rerankModelId"
:placeholder="$t('documentCollection.placeholder.rerankLlm')"
>
<ElOption
v-for="item in rerankerLlmList"
:key="item.id"
:label="item.title"
:value="item.id || ''"
/>
</ElSelect>
</ElFormItem>
<ElFormItem
prop="searchEngineEnable"
:label="$t('documentCollection.searchEngineEnable')"
>
<ElSwitch v-model="entity.searchEngineEnable" />
</ElFormItem>
</ElForm>
<template #footer>
<ElButton @click="closeDialog">
{{ $t('button.cancel') }}
</ElButton>
<ElButton
type="primary"
@click="save"
:loading="btnLoading"
:disabled="btnLoading"
>
{{ $t('button.save') }}
</ElButton>
</template>
</ElDialog>
</template>
<style scoped></style>

View File

@@ -0,0 +1,160 @@
<script setup lang="ts">
import { ref } from 'vue';
import { $t } from '@easyflow/locales';
import { Delete, Download, MoreFilled } from '@element-plus/icons-vue';
import {
ElButton,
ElDropdown,
ElDropdownItem,
ElDropdownMenu,
ElImage,
ElMessage,
ElMessageBox,
ElTable,
ElTableColumn,
} from 'element-plus';
import { api } from '#/api/request';
import documentIcon from '#/assets/ai/knowledge/document.svg';
import PageData from '#/components/page/PageData.vue';
const props = defineProps({
knowledgeId: {
required: true,
type: String,
},
});
const emits = defineEmits(['viewDoc']);
defineExpose({
search(searchText: string) {
pageDataRef.value.setQuery({
title: searchText,
});
},
});
const pageDataRef = ref();
const handleView = (row: any) => {
emits('viewDoc', row.id);
};
const handleDownload = (row: any) => {
window.open(row.documentPath, '_blank');
};
const handleDelete = (row: any) => {
ElMessageBox.confirm($t('message.deleteAlert'), $t('message.noticeTitle'), {
confirmButtonText: $t('button.confirm'),
cancelButtonText: $t('button.cancel'),
type: 'warning',
}).then(() => {
api.post('/api/v1/document/removeDoc', { id: row.id }).then((res) => {
if (res.errorCode === 0) {
ElMessage.success($t('message.deleteOkMessage'));
pageDataRef.value.setQuery({ id: props.knowledgeId });
}
});
// 删除逻辑
});
};
</script>
<template>
<PageData
page-url="/api/v1/document/documentList"
ref="pageDataRef"
:page-size="10"
:extra-query-params="{
id: props.knowledgeId,
sort: 'desc',
sortKey: 'created',
}"
>
<template #default="{ pageList }">
<ElTable :data="pageList" style="width: 100%" size="large">
<ElTableColumn
prop="fileName"
:label="$t('documentCollection.fileName')"
>
<template #default="{ row }">
<span class="file-name-container">
<ElImage :src="documentIcon" class="mr-1" />
<span class="title">
{{ row.title }}
</span>
</span>
</template>
</ElTableColumn>
<ElTableColumn
prop="documentType"
:label="$t('documentCollection.documentType')"
width="180"
/>
<ElTableColumn
prop="chunkCount"
:label="$t('documentCollection.knowledgeCount')"
width="180"
/>
<ElTableColumn
:label="$t('documentCollection.createdModifyTime')"
width="200"
>
<template #default="{ row }">
<div class="time-container">
<span>{{ row.created }}</span>
<span>{{ row.modified }}</span>
</div>
</template>
</ElTableColumn>
<ElTableColumn :label="$t('common.handle')" width="120" align="right">
<template #default="{ row }">
<div class="flex items-center gap-3">
<ElButton link type="primary" @click="handleView(row)">
{{ $t('button.viewSegmentation') }}
</ElButton>
<ElDropdown>
<ElButton link :icon="MoreFilled" />
<template #dropdown>
<ElDropdownMenu>
<ElDropdownItem @click="handleDownload(row)">
<ElButton link :icon="Download">
{{ $t('button.download') }}
</ElButton>
</ElDropdownItem>
<ElDropdownItem @click="handleDelete(row)">
<ElButton link :icon="Delete" type="danger">
{{ $t('button.delete') }}
</ElButton>
</ElDropdownItem>
</ElDropdownMenu>
</template>
</ElDropdown>
</div>
</template>
</ElTableColumn>
</ElTable>
</template>
</PageData>
</template>
<style scoped>
.time-container {
display: flex;
flex-direction: column;
justify-content: space-between;
}
.file-name-container {
display: flex;
align-items: center;
}
.title {
font-weight: 500;
font-size: 14px;
color: #1a1a1a;
line-height: 20px;
text-align: left;
font-style: normal;
text-transform: none;
}
</style>

View File

@@ -0,0 +1,253 @@
<script setup lang="ts">
import { ref } from 'vue';
import { $t } from '@easyflow/locales';
import { Back } from '@element-plus/icons-vue';
import {
ElButton,
ElMessage,
ElPagination,
ElStep,
ElSteps,
} from 'element-plus';
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';
const emits = defineEmits(['importBack']);
const back = () => {
emits('importBack');
};
const files = ref([]);
const splitterParams = ref({});
const activeStep = ref(0);
const fileUploadRef = ref();
const confirmImportRef = ref();
const segmenterDocRef = ref();
const pagination = ref({
pageSize: 10,
currentPage: 1,
total: 0,
});
const goToNextStep = () => {
if (activeStep.value === 0) {
if (fileUploadRef.value.getFilesData().length === 0) {
ElMessage.error($t('message.uploadFileFirst'));
return;
}
files.value = fileUploadRef.value.getFilesData();
}
if (activeStep.value === 1 && segmenterDocRef.value) {
splitterParams.value = segmenterDocRef.value.getSplitterFormValues();
}
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');
};
</script>
<template>
<div class="imp-doc-kno-container">
<div class="imp-doc-header">
<ElButton @click="back" :icon="Back">
{{ $t('button.back') }}
</ElButton>
</div>
<div class="imp-doc-kno-content">
<div class="rounded-lg bg-[var(--table-header-bg-color)] py-5">
<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>
</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>
<ElButton @click="goToPreviousStep" type="primary" v-if="activeStep >= 1">
{{ $t('button.previousStep') }}
</ElButton>
<ElButton @click="goToNextStep" type="primary" v-if="activeStep < 3">
{{ $t('button.nextStep') }}
</ElButton>
<ElButton
@click="confirmImport"
type="primary"
v-if="activeStep === 3"
:loading="loadingSave"
:disabled="loadingSave"
>
{{ $t('button.startImport') }}
</ElButton>
</div>
</div>
</template>
<style scoped>
.imp-doc-kno-container {
position: relative;
height: 100%;
background-color: var(--el-bg-color);
border-radius: 12px;
padding: 20px;
display: flex;
flex-direction: column;
}
.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;
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);
}
: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);
}
: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);
}
</style>

View File

@@ -0,0 +1,107 @@
<script setup lang="ts">
import { ref } from 'vue';
import { $t } from '@easyflow/locales';
import { ElButton, ElProgress, ElTable, ElTableColumn } from 'element-plus';
import { formatFileSize } from '#/api/common/file';
import DragFileUpload from '#/components/upload/DragFileUpload.vue';
interface FileInfo {
uid: string;
fileName: string;
progressUpload: number;
fileSize: number;
status: string;
filePath: string;
}
const fileData = ref<FileInfo[]>([]);
const filesPath = ref([]);
defineExpose({
getFilesData() {
return fileData.value;
},
});
function handleSuccess(response: any) {
filesPath.value = response.data;
}
function handleChange(file: any) {
const existingFile = fileData.value.find((item) => item.uid === file.uid);
if (existingFile) {
fileData.value = fileData.value.map((item) => {
if (item.uid === file.uid) {
return {
...item,
fileSize: file.size,
progressUpload: file.percentage,
status: file.status,
filePath: file?.response?.data?.path,
};
}
return item;
});
} else {
fileData.value.push({
uid: file.uid,
fileName: file.name,
progressUpload: file.percentage,
fileSize: file.size,
status: file.status,
filePath: file?.response?.data?.path,
});
}
}
function handleRemove(row: any) {
fileData.value = fileData.value.filter((item) => item.uid !== row.uid);
}
</script>
<template>
<div>
<div>
<DragFileUpload @success="handleSuccess" @on-change="handleChange" />
</div>
<div>
<ElTable :data="fileData" style="width: 100%" size="large">
<ElTableColumn
prop="fileName"
:label="$t('documentCollection.importDoc.fileName')"
width="250"
/>
<ElTableColumn
prop="progressUpload"
:label="$t('documentCollection.importDoc.progressUpload')"
width="180"
>
<template #default="{ row }">
<ElProgress
:percentage="row.progressUpload"
v-if="row.status === 'success'"
status="success"
/>
<ElProgress v-else :percentage="row.progressUpload" />
</template>
</ElTableColumn>
<ElTableColumn
prop="fileSize"
:label="$t('documentCollection.importDoc.fileSize')"
>
<template #default="{ row }">
<span>{{ formatFileSize(row.fileSize) }}</span>
</template>
</ElTableColumn>
<ElTableColumn :label="$t('common.handle')">
<template #default="{ row }">
<ElButton type="danger" size="small" @click="handleRemove(row)">
{{ $t('button.delete') }}
</ElButton>
</template>
</ElTableColumn>
</ElTable>
</div>
</div>
</template>
<style scoped></style>

View File

@@ -0,0 +1,75 @@
<script setup lang="ts">
import { ref } from 'vue';
import { $t } from '@easyflow/locales';
import { ElButton, ElInput, ElMessage } from 'element-plus';
import { api } from '#/api/request';
import PreviewSearchKnowledge from '#/views/ai/documentCollection/PreviewSearchKnowledge.vue';
const props = defineProps({
knowledgeId: {
type: String,
required: true,
},
});
const searchDataList = ref([]);
const keyword = ref('');
const previewSearchKnowledgeRef = ref();
const handleSearch = () => {
if (!keyword.value) {
ElMessage.error($t('message.pleaseInputContent'));
return;
}
previewSearchKnowledgeRef.value.loadingContent(true);
api
.get(
`/api/v1/documentCollection/search?knowledgeId=${props.knowledgeId}&keyword=${keyword.value}`,
)
.then((res) => {
previewSearchKnowledgeRef.value.loadingContent(false);
searchDataList.value = res.data;
});
};
</script>
<template>
<div class="search-container">
<div class="search-input">
<ElInput
v-model="keyword"
:placeholder="$t('common.searchPlaceholder')"
/>
<ElButton type="primary" @click="handleSearch">
{{ $t('button.query') }}
</ElButton>
</div>
<div class="search-result">
<PreviewSearchKnowledge
:data="searchDataList"
ref="previewSearchKnowledgeRef"
/>
</div>
</div>
</template>
<style scoped>
.search-container {
width: 100%;
height: 100%;
padding: 0 0 20px 0;
display: flex;
flex-direction: column;
}
.search-input {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.search-result {
padding-top: 20px;
flex: 1;
}
</style>

View File

@@ -0,0 +1,342 @@
<script setup lang="ts">
import { onMounted, reactive, ref } from 'vue';
import { $t } from '@easyflow/locales';
import { InfoFilled } from '@element-plus/icons-vue';
import {
ElButton,
ElForm,
ElFormItem,
ElInputNumber,
ElMessage,
ElOption,
ElSelect,
ElSwitch,
ElTooltip,
} from 'element-plus';
import { api } from '#/api/request';
const props = defineProps({
documentCollectionId: {
type: String,
required: true,
},
});
onMounted(() => {
getDocumentCollectionConfig();
});
const searchEngineEnable = ref(false);
const getDocumentCollectionConfig = () => {
api
.get(`/api/v1/documentCollection/detail?id=${props.documentCollectionId}`)
.then((res) => {
const { data } = res;
searchConfig.docRecallMaxNum = data.options.docRecallMaxNum
? Number(data.options.docRecallMaxNum)
: 5;
searchConfig.simThreshold = data.options.simThreshold
? Number(data.options.simThreshold)
: 0.5;
searchConfig.searchEngineType = data.options.searchEngineType || 'lucene';
searchEngineEnable.value = !!data.searchEngineEnable;
});
};
const searchConfig = reactive({
docRecallMaxNum: 5,
simThreshold: 0.5,
searchEngineType: 'lucene',
});
const submitConfig = () => {
const submitData = {
id: props.documentCollectionId,
options: {
docRecallMaxNum: searchConfig.docRecallMaxNum,
simThreshold: searchConfig.simThreshold,
searchEngineType: searchConfig.searchEngineType,
},
searchEngineEnable: searchEngineEnable.value,
};
api
.post('/api/v1/documentCollection/update', submitData)
.then(() => {
ElMessage.success($t('documentCollectionSearch.message.saveSuccess'));
})
.catch((error) => {
ElMessage.error($t('documentCollectionSearch.message.saveFailed'));
console.error('保存配置失败:', error);
});
};
const searchEngineOptions = [
{
label: 'Lucene',
value: 'lucene',
},
{
label: 'ElasticSearch',
value: 'elasticSearch',
},
];
const handleSearchEngineEnableChange = () => {
api.post('/api/v1/documentCollection/update', {
id: props.documentCollectionId,
searchEngineEnable: searchEngineEnable.value,
});
};
</script>
<template>
<div class="search-config-sidebar">
<div class="config-header">
<h3>{{ $t('documentCollectionSearch.title') }}</h3>
</div>
<ElForm
class="config-form"
:model="searchConfig"
label-width="100%"
size="small"
>
<ElFormItem prop="docRecallMaxNum" class="form-item">
<div class="form-item-label">
<span>{{
$t('documentCollectionSearch.docRecallMaxNum.label')
}}</span>
<ElTooltip
:content="$t('documentCollectionSearch.docRecallMaxNum.tooltip')"
placement="top"
effect="dark"
class="label-tooltip"
>
<InfoFilled class="info-icon" />
</ElTooltip>
</div>
<div class="form-item-content">
<ElInputNumber
v-model="searchConfig.docRecallMaxNum"
:min="1"
:max="50"
:step="1"
:placeholder="$t('documentCollectionSearch.placeholder.count')"
class="form-control"
>
<template #append>
{{ $t('documentCollectionSearch.unit.count') }}
</template>
</ElInputNumber>
</div>
</ElFormItem>
<ElFormItem prop="simThreshold" class="form-item">
<div class="form-item-label">
<span>{{ $t('documentCollectionSearch.simThreshold.label') }}</span>
<ElTooltip
:content="$t('documentCollectionSearch.simThreshold.tooltip')"
placement="top"
effect="dark"
class="label-tooltip"
>
<InfoFilled class="info-icon" />
</ElTooltip>
</div>
<div class="form-item-content">
<ElInputNumber
v-model="searchConfig.simThreshold"
:min="0"
:max="1"
:step="0.01"
show-input
class="form-control"
/>
</div>
</ElFormItem>
<!-- 搜索引擎启用开关 -->
<ElFormItem class="form-item">
<div class="form-item-label">
<span>{{
$t('documentCollectionSearch.searchEngineEnable.label')
}}</span>
<ElTooltip
:content="$t('documentCollectionSearch.searchEngineEnable.tooltip')"
placement="top"
effect="dark"
class="label-tooltip"
>
<InfoFilled class="info-icon" />
</ElTooltip>
</div>
<div class="form-item-content">
<ElSwitch
v-model="searchEngineEnable"
@change="handleSearchEngineEnableChange"
:active-text="$t('documentCollectionSearch.switch.on')"
:inactive-text="$t('documentCollectionSearch.switch.off')"
class="form-control switch-control"
/>
</div>
</ElFormItem>
<!-- 通过 searchEngineEnable 控制显示/隐藏 -->
<ElFormItem
v-if="searchEngineEnable"
prop="searchEngineType"
class="form-item"
>
<div class="form-item-label">
<span>{{
$t('documentCollectionSearch.searchEngineType.label')
}}</span>
<ElTooltip
:content="$t('documentCollectionSearch.searchEngineType.tooltip')"
placement="top"
effect="dark"
class="label-tooltip"
>
<InfoFilled class="info-icon" />
</ElTooltip>
</div>
<div class="form-item-content">
<ElSelect
v-model="searchConfig.searchEngineType"
:placeholder="
$t('documentCollectionSearch.searchEngineType.placeholder')
"
class="form-control"
>
<ElOption
v-for="option in searchEngineOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</ElSelect>
</div>
</ElFormItem>
</ElForm>
<div class="config-footer">
<ElButton type="primary" @click="submitConfig" class="submit-btn">
{{ $t('documentCollectionSearch.button.save') }}
</ElButton>
</div>
</div>
</template>
<style scoped>
.search-config-sidebar {
width: 60%;
height: 100%;
padding: 16px;
box-sizing: border-box;
overflow-y: auto;
overflow-x: hidden;
}
.config-header {
margin-bottom: 16px;
border-bottom: 1px solid #e6e6e6;
padding-bottom: 8px;
}
.config-header h3 {
margin: 0;
font-size: 15px;
font-weight: 600;
}
.config-form {
margin-bottom: 24px;
}
.form-item {
margin-bottom: 20px;
display: flex;
flex-direction: column;
gap: 12px;
}
.form-item-label {
display: flex;
align-items: center;
gap: 4px;
font-size: 14px;
color: #606266;
line-height: 1.4;
}
.label-tooltip {
display: inline-block;
cursor: help;
}
:deep(.form-item .el-form-item__content) {
width: 100%;
margin-left: 0 !important;
}
.form-item-content {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
margin-top: 4px;
}
.form-control {
flex: 1;
width: 100%;
min-width: 0;
}
.switch-control {
width: auto;
flex: none;
min-width: 80px;
}
.info-icon {
font-size: 14px;
color: #909399;
cursor: help;
width: 16px;
height: 16px;
flex-shrink: 0;
flex: none;
}
.info-icon:hover {
color: #409eff;
}
.submit-btn {
width: 100%;
padding: 8px 0;
}
.config-footer {
position: sticky;
bottom: 0;
padding-top: 8px;
}
:deep(.el-form-item__content) {
width: 100%;
box-sizing: border-box;
}
:deep(.el-slider) {
--el-slider-input-width: 60px;
}
:deep(.el-input-number),
:deep(.el-select) {
width: 100%;
}
</style>

View File

@@ -0,0 +1,251 @@
<script setup lang="ts">
import { ref } from 'vue';
import { Document } from '@element-plus/icons-vue';
import { ElButton, ElIcon } from 'element-plus';
// 定义类型接口(与 React 版本一致)
interface PreviewItem {
sorting: string;
content: string;
score: string;
}
const props = defineProps({
hideScore: {
type: Boolean,
default: false,
},
data: {
type: Array as () => PreviewItem[],
default: () => [],
},
total: {
type: Number,
default: 0,
},
loading: {
type: Boolean,
default: false,
},
confirmImport: {
type: Boolean,
default: false,
},
disabledConfirm: {
type: Boolean,
default: false,
},
onCancel: {
type: Function,
default: () => {},
},
onConfirm: {
type: Function,
default: () => {},
},
isSearching: {
type: Boolean,
default: false,
},
});
const loadingStatus = ref(false);
defineExpose({
loadingContent: (state: boolean) => {
loadingStatus.value = state;
},
});
</script>
<template>
<div class="preview-container" v-loading="loadingStatus">
<!-- 头部区域标题 + 统计信息 -->
<div class="preview-header">
<h3>
<ElIcon class="header-icon" size="20">
<Document />
</ElIcon>
{{
isSearching
? $t('documentCollection.searchResults')
: $t('documentCollection.documentPreview')
}}
</h3>
<span class="preview-stats" v-if="props.data.length > 0">
{{ $t('documentCollection.total') }}
{{ total > 0 ? total : data.length }}
{{ $t('documentCollection.segments') }}
</span>
</div>
<!-- 内容区域列表预览 -->
<div class="preview-content">
<div class="preview-list">
<div
v-for="(item, index) in data"
:key="index"
class="el-list-item-container"
>
<div class="el-list-item">
<div class="segment-badge">
{{ item.sorting ?? index + 1 }}
</div>
<div class="el-list-item-meta">
<div v-if="!hideScore">
{{ $t('documentCollection.similarityScore') }}: {{ item.score }}
</div>
<div class="content-desc">{{ item.content }}</div>
</div>
</div>
</div>
</div>
</div>
<!-- 操作按钮区域仅导入确认模式显示 -->
<div class="preview-actions" v-if="confirmImport">
<div class="action-buttons">
<ElButton
:style="{ minWidth: '100px', height: '36px' }"
click="onCancel"
>
{{ $t('documentCollection.actions.confirmImport') }}
</ElButton>
<ElButton
type="primary"
:style="{ minWidth: '100px', height: '36px' }"
:loading="disabledConfirm"
click="onConfirm"
>
{{ $t('documentCollection.actions.cancelImport') }}
</ElButton>
</div>
</div>
</div>
</template>
<style scoped>
.preview-container {
width: 100%;
height: 100%;
overflow: hidden;
background-color: var(--el-bg-color);
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgb(0 0 0 / 8%);
.preview-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid var(--el-border-color);
h3 {
display: flex;
gap: 8px;
align-items: center;
margin: 0;
font-size: 16px;
font-weight: 500;
color: var(--el-text-color-primary);
.header-icon {
color: var(--el-color-primary);
}
}
.preview-stats {
font-size: 14px;
color: var(--el-text-color-secondary);
}
}
.preview-content {
padding: 20px;
overflow-y: auto;
.preview-list {
.segment-badge {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
font-size: 12px;
font-weight: 500;
color: var(--el-color-primary);
background-color: var(--el-color-primary-light-9);
border-radius: 50%;
}
.similarity-score {
font-size: 14px;
color: var(--el-color-primary);
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
.content-desc {
width: 100%;
padding: 12px;
font-size: 14px;
line-height: 1.6;
white-space: pre-wrap;
background-color: var(--el-bg-color);
border-left: 3px solid #e2e8f0;
border-radius: 6px;
transition: all 0.2s;
&:hover {
border-color: #4361ee;
box-shadow: 0 4px 12px rgb(67 97 238 / 8%);
transform: translateY(-2px);
}
}
.el-list-item {
display: flex;
gap: 12px;
align-items: center;
padding: 18px;
border-radius: 8px;
}
.el-list-item-meta {
display: flex;
flex: 1;
flex-direction: column;
gap: 12px;
align-items: flex-start;
}
}
}
.preview-actions {
padding: 16px 20px;
background-color: var(--el-bg-color-page);
border-top: 1px solid var(--el-border-color);
.action-buttons {
display: flex;
gap: 12px;
justify-content: flex-end;
}
}
}
/* 适配 Element Plus 加载状态样式 */
.el-list--loading .el-list-loading {
padding: 40px 0;
}
.el-list-item {
width: 100%;
margin-top: 12px;
border: 1px solid var(--el-border-color-lighter);
&:hover {
border-color: var(--el-color-primary);
}
}
</style>

View File

@@ -0,0 +1,189 @@
<script setup lang="ts">
import { reactive, ref } from 'vue';
import { $t } from '@easyflow/locales';
import {
ElForm,
ElFormItem,
ElInput,
ElOption,
ElSelect,
ElSlider,
} from 'element-plus';
const formRef = ref();
const form = reactive({
fileType: 'doc',
splitterName: 'SimpleDocumentSplitter',
chunkSize: 512,
overlapSize: 128,
regex: '',
rowsPerChunk: 0,
mdSplitterLevel: 1,
});
const fileTypes = [
{
label: $t('documentCollection.splitterDoc.document'),
value: 'doc',
},
];
const splitterNames = [
{
label: $t('documentCollection.splitterDoc.simpleDocumentSplitter'),
value: 'SimpleDocumentSplitter',
},
{
label: $t('documentCollection.splitterDoc.simpleTokenizeSplitter'),
value: 'SimpleTokenizeSplitter',
},
{
label: $t('documentCollection.splitterDoc.regexDocumentSplitter'),
value: 'RegexDocumentSplitter',
},
{
label: $t('documentCollection.splitterDoc.markdownHeaderSplitter'),
value: 'MarkdownHeaderSplitter',
},
];
const mdSplitterLevel = [
{
label: '#',
value: 1,
},
{
label: '##',
value: 2,
},
{
label: '###',
value: 3,
},
{
label: '####',
value: 4,
},
{
label: '#####',
value: 5,
},
{
label: '######',
value: 6,
},
];
const rules = {
name: [
{ required: true, message: 'Please input Activity name', trigger: 'blur' },
],
region: [
{
required: true,
message: 'Please select Activity zone',
trigger: 'change',
},
],
};
defineExpose({
getSplitterFormValues() {
return form;
},
});
</script>
<template>
<div class="splitter-doc-container">
<ElForm
ref="formRef"
:model="form"
:rules="rules"
label-width="auto"
class="custom-form"
>
<ElFormItem
:label="$t('documentCollection.splitterDoc.fileType')"
prop="fileType"
>
<ElSelect v-model="form.fileType">
<ElOption
v-for="item in fileTypes"
:key="item.value"
v-bind="item"
:label="item.label"
/>
</ElSelect>
</ElFormItem>
<ElFormItem
:label="$t('documentCollection.splitterDoc.splitterName')"
prop="splitterName"
>
<ElSelect v-model="form.splitterName">
<ElOption
v-for="item in splitterNames"
:key="item.value"
v-bind="item"
:label="item.label"
/>
</ElSelect>
</ElFormItem>
<ElFormItem
:label="$t('documentCollection.splitterDoc.chunkSize')"
v-if="
form.splitterName === 'SimpleDocumentSplitter' ||
form.splitterName === 'SimpleTokenizeSplitter'
"
prop="chunkSize"
>
<ElSlider v-model="form.chunkSize" show-input :max="2048" />
</ElFormItem>
<ElFormItem
:label="$t('documentCollection.splitterDoc.overlapSize')"
v-if="
form.splitterName === 'SimpleDocumentSplitter' ||
form.splitterName === 'SimpleTokenizeSplitter'
"
prop="overlapSize"
>
<ElSlider v-model="form.overlapSize" show-input :max="2048" />
</ElFormItem>
<ElFormItem
:label="$t('documentCollection.splitterDoc.regex')"
prop="regex"
v-if="form.splitterName === 'RegexDocumentSplitter'"
>
<ElInput v-model="form.regex" />
</ElFormItem>
<ElFormItem
v-if="form.splitterName === 'MarkdownHeaderSplitter'"
:label="$t('documentCollection.splitterDoc.mdSplitterLevel')"
prop="splitterName"
>
<ElSelect v-model="form.mdSplitterLevel">
<ElOption
v-for="item in mdSplitterLevel"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</ElSelect>
</ElFormItem>
</ElForm>
</div>
</template>
<style scoped>
.splitter-doc-container {
height: 100%;
width: 100%;
align-items: center;
display: flex;
justify-content: center;
}
.custom-form {
width: 500px;
}
.custom-form :deep(.el-input),
.custom-form :deep(.ElSelect) {
width: 100%;
}
</style>

View File

@@ -0,0 +1,168 @@
<script setup lang="ts">
import { onMounted, ref, watch } from 'vue';
import { useRoute } from 'vue-router';
import { api } from '#/api/request';
import CategoryPanel from '#/components/categoryPanel/CategoryPanel.vue';
import PreviewSearchKnowledge from '#/views/ai/documentCollection/PreviewSearchKnowledge.vue';
export interface FileInfo {
filePath: string;
fileName: string;
}
const props = defineProps({
pageNumber: {
default: 1,
type: Number,
},
pageSize: {
default: 10,
type: Number,
},
knowledgeId: {
default: '',
type: String,
},
fliesList: {
default: () => [],
type: Array<FileInfo>,
},
splitterParams: {
default: () => {},
type: Object,
},
});
const emit = defineEmits(['updateTotal']);
const documentList = ref<any[]>([]);
const route = useRoute();
defineExpose({
getFilesData() {
return documentList.value.length;
},
});
const knowledgeIdRef = ref<string>((route.query.id as string) || '');
const selectedCategory = ref<any>();
watch(
() => props.pageNumber,
(newVal) => {
if (selectedCategory.value) {
splitterDocPreview(
newVal,
props.pageSize,
selectedCategory.value.value,
'textSplit',
selectedCategory.value.label,
);
} else {
splitterDocPreview(
newVal,
props.pageSize,
props.fliesList[0]!.filePath,
'textSplit',
props.fliesList[0]!.fileName,
);
}
},
);
watch(
() => props.pageSize,
(newVal) => {
if (selectedCategory.value) {
splitterDocPreview(
props.pageNumber,
newVal,
selectedCategory.value.value,
'textSplit',
selectedCategory.value.label,
);
} else {
splitterDocPreview(
props.pageNumber,
newVal,
props.fliesList[0]!.filePath,
'textSplit',
props.fliesList[0]!.fileName,
);
}
},
);
function splitterDocPreview(
pageNumber: number,
pageSize: number,
filePath: string,
operation: string,
fileOriginName: string,
) {
api
.post('/api/v1/document/textSplit', {
pageNumber,
pageSize,
filePath,
operation,
knowledgeId: knowledgeIdRef.value,
fileOriginName,
...props.splitterParams,
})
.then((res) => {
if (res.errorCode === 0) {
documentList.value = res.data.previewData;
emit('updateTotal', res.data.total);
}
});
}
onMounted(() => {
if (props.fliesList.length === 0) {
return;
}
splitterDocPreview(
props.pageNumber,
props.pageSize,
props.fliesList[0]!.filePath,
'textSplit',
props.fliesList[0]!.fileName,
);
});
const changeCategory = (category: any) => {
selectedCategory.value = category;
splitterDocPreview(
props.pageNumber,
props.pageSize,
category.value,
'textSplit',
category.label,
);
};
</script>
<template>
<div class="splitter-doc-container">
<div>
<CategoryPanel
:categories="fliesList"
title-key="fileName"
:need-hide-collapse="true"
:expand-width="200"
value-key="filePath"
:default-selected-category="fliesList[0]!.filePath"
@click="changeCategory"
/>
</div>
<div class="preview-container">
<PreviewSearchKnowledge :data="documentList" :hide-score="true" />
</div>
</div>
</template>
<style scoped>
.splitter-doc-container {
height: 100%;
display: flex;
}
.preview-container {
flex: 1;
overflow: scroll;
}
</style>