初始化
This commit is contained in:
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user