增加知识库模块
This commit is contained in:
77
src/api/knowledge/dataset/index.ts
Normal file
77
src/api/knowledge/dataset/index.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { newService } from '/@/utils/request';
|
||||
|
||||
// 数据集查询参数
|
||||
export interface DatasetQueryParams {
|
||||
keyword?: string;
|
||||
status?: string;
|
||||
pageNum: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
// 数据集信息
|
||||
export interface DatasetInfo {
|
||||
id?: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
type: string; // text, qa, table
|
||||
documentCount?: number;
|
||||
charCount?: number;
|
||||
status: string; // enable, disable
|
||||
embeddingModel?: string;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
// 获取数据集列表
|
||||
export function listDatasets(params: DatasetQueryParams) {
|
||||
return newService({
|
||||
url: '/knowledge/dataset/list',
|
||||
method: 'get',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
// 获取数据集详情
|
||||
export function getDataset(id: string) {
|
||||
return newService({
|
||||
url: '/knowledge/dataset/detail',
|
||||
method: 'get',
|
||||
params: { id },
|
||||
});
|
||||
}
|
||||
|
||||
// 创建数据集
|
||||
export function createDataset(data: DatasetInfo) {
|
||||
return newService({
|
||||
url: '/knowledge/dataset/create',
|
||||
method: 'post',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
// 更新数据集
|
||||
export function updateDataset(data: DatasetInfo) {
|
||||
return newService({
|
||||
url: '/knowledge/dataset/update',
|
||||
method: 'put',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
// 删除数据集
|
||||
export function deleteDataset(id: string) {
|
||||
return newService({
|
||||
url: '/knowledge/dataset/delete',
|
||||
method: 'delete',
|
||||
params: { id },
|
||||
});
|
||||
}
|
||||
|
||||
// 更新数据集状态
|
||||
export function updateDatasetStatus(data: { id: string; status: string }) {
|
||||
return newService({
|
||||
url: '/knowledge/dataset/updateStatus',
|
||||
method: 'put',
|
||||
data,
|
||||
});
|
||||
}
|
||||
134
src/api/knowledge/document/index.ts
Normal file
134
src/api/knowledge/document/index.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { newService } from '/@/utils/request';
|
||||
|
||||
// 文档查询参数
|
||||
export interface DocumentQueryParams {
|
||||
keyword?: string;
|
||||
datasetId?: string;
|
||||
status?: string;
|
||||
fileType?: string;
|
||||
pageNum: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
// 文档信息
|
||||
export interface DocumentInfo {
|
||||
id?: string;
|
||||
name: string;
|
||||
datasetId: string;
|
||||
datasetName?: string;
|
||||
fileType: string; // pdf, docx, txt, md, html
|
||||
fileSize?: number;
|
||||
filePath?: string;
|
||||
charCount?: number;
|
||||
chunkCount?: number;
|
||||
status: string; // pending, processing, completed, failed
|
||||
indexStatus?: string; // not_indexed, indexing, indexed, failed
|
||||
errorMessage?: string;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
// 文档分段信息
|
||||
export interface DocumentChunk {
|
||||
id: string;
|
||||
documentId: string;
|
||||
content: string;
|
||||
chunkIndex: number;
|
||||
charCount: number;
|
||||
tokenCount?: number;
|
||||
embedding?: number[];
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
// 获取文档列表
|
||||
export function listDocuments(params: DocumentQueryParams) {
|
||||
return newService({
|
||||
url: '/knowledge/document/list',
|
||||
method: 'get',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
// 获取文档详情
|
||||
export function getDocument(id: string) {
|
||||
return newService({
|
||||
url: '/knowledge/document/detail',
|
||||
method: 'get',
|
||||
params: { id },
|
||||
});
|
||||
}
|
||||
|
||||
// 上传文档
|
||||
export function uploadDocument(data: FormData) {
|
||||
return newService({
|
||||
url: '/knowledge/document/upload',
|
||||
method: 'post',
|
||||
data,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 删除文档
|
||||
export function deleteDocument(id: string) {
|
||||
return newService({
|
||||
url: '/knowledge/document/delete',
|
||||
method: 'delete',
|
||||
params: { id },
|
||||
});
|
||||
}
|
||||
|
||||
// 批量删除文档
|
||||
export function batchDeleteDocuments(ids: string[]) {
|
||||
return newService({
|
||||
url: '/knowledge/document/batchDelete',
|
||||
method: 'delete',
|
||||
data: { ids },
|
||||
});
|
||||
}
|
||||
|
||||
// 重新处理文档
|
||||
export function reprocessDocument(id: string) {
|
||||
return newService({
|
||||
url: '/knowledge/document/reprocess',
|
||||
method: 'post',
|
||||
params: { id },
|
||||
});
|
||||
}
|
||||
|
||||
// 获取文档分段列表
|
||||
export function listDocumentChunks(params: { documentId: string; pageNum: number; pageSize: number }) {
|
||||
return newService({
|
||||
url: '/knowledge/document/chunks',
|
||||
method: 'get',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
// 更新文档分段
|
||||
export function updateDocumentChunk(data: { id: string; content: string }) {
|
||||
return newService({
|
||||
url: '/knowledge/document/chunk/update',
|
||||
method: 'put',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
// 删除文档分段
|
||||
export function deleteDocumentChunk(id: string) {
|
||||
return newService({
|
||||
url: '/knowledge/document/chunk/delete',
|
||||
method: 'delete',
|
||||
params: { id },
|
||||
});
|
||||
}
|
||||
|
||||
// 预览文档内容
|
||||
export function previewDocument(id: string) {
|
||||
return newService({
|
||||
url: '/knowledge/document/preview',
|
||||
method: 'get',
|
||||
params: { id },
|
||||
});
|
||||
}
|
||||
206
src/views/knowledge/dataset/component/editDataset.vue
Normal file
206
src/views/knowledge/dataset/component/editDataset.vue
Normal file
@@ -0,0 +1,206 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:title="isEdit ? '编辑数据集' : '新增数据集'"
|
||||
v-model="isShowDialog"
|
||||
width="600px"
|
||||
:close-on-click-modal="false"
|
||||
@close="onCancel"
|
||||
>
|
||||
<el-form ref="formRef" :model="ruleForm" :rules="rules" label-width="100px">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="24">
|
||||
<el-form-item label="数据集名称" prop="name">
|
||||
<el-input v-model="ruleForm.name" placeholder="请输入数据集名称" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="数据集类型" prop="type">
|
||||
<el-select v-model="ruleForm.type" placeholder="请选择类型" clearable style="width: 100%">
|
||||
<el-option label="文本" value="text" />
|
||||
<el-option label="问答" value="qa" />
|
||||
<el-option label="表格" value="table" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="向量模型" prop="embeddingModel">
|
||||
<el-select v-model="ruleForm.embeddingModel" placeholder="请选择向量模型" clearable style="width: 100%">
|
||||
<el-option label="text-embedding-ada-002" value="text-embedding-ada-002" />
|
||||
<el-option label="text-embedding-3-small" value="text-embedding-3-small" />
|
||||
<el-option label="text-embedding-3-large" value="text-embedding-3-large" />
|
||||
<el-option label="bge-large-zh" value="bge-large-zh" />
|
||||
<el-option label="m3e-base" value="m3e-base" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="24">
|
||||
<el-form-item label="描述" prop="description">
|
||||
<el-input v-model="ruleForm.description" type="textarea" :rows="4" placeholder="请输入数据集描述" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20" v-if="isEdit">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="文档数量">
|
||||
<el-input :value="ruleForm.documentCount" disabled />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="字符数量">
|
||||
<el-input :value="ruleForm.charCount" disabled />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="onCancel">取 消</el-button>
|
||||
<el-button type="primary" @click="onSubmit" :loading="submitLoading">{{ isEdit ? '保 存' : '创 建' }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'editDataset',
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import type { FormInstance, FormRules } from 'element-plus';
|
||||
import { createDataset, updateDataset, getDataset } from '/@/api/knowledge/dataset';
|
||||
|
||||
// 定义事件
|
||||
const emit = defineEmits(['getDatasetList']);
|
||||
|
||||
// 表单ref
|
||||
const formRef = ref<FormInstance>();
|
||||
|
||||
// 弹窗状态
|
||||
const isShowDialog = ref(false);
|
||||
const isEdit = ref(false);
|
||||
const submitLoading = ref(false);
|
||||
|
||||
// 表单数据
|
||||
const ruleForm = reactive({
|
||||
id: '',
|
||||
name: '',
|
||||
type: 'text',
|
||||
description: '',
|
||||
embeddingModel: 'text-embedding-ada-002',
|
||||
documentCount: 0,
|
||||
charCount: 0,
|
||||
});
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive<FormRules>({
|
||||
name: [{ required: true, message: '请输入数据集名称', trigger: 'blur' }],
|
||||
type: [{ required: true, message: '请选择数据集类型', trigger: 'change' }],
|
||||
embeddingModel: [{ required: true, message: '请选择向量模型', trigger: 'change' }],
|
||||
});
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
ruleForm.id = '';
|
||||
ruleForm.name = '';
|
||||
ruleForm.type = 'text';
|
||||
ruleForm.description = '';
|
||||
ruleForm.embeddingModel = 'text-embedding-ada-002';
|
||||
ruleForm.documentCount = 0;
|
||||
ruleForm.charCount = 0;
|
||||
};
|
||||
|
||||
// 打开弹窗
|
||||
const openDialog = async (row?: any) => {
|
||||
resetForm();
|
||||
isEdit.value = !!row;
|
||||
|
||||
if (row) {
|
||||
try {
|
||||
const res: any = await getDataset(row.id);
|
||||
const data = res.data || row;
|
||||
ruleForm.id = data.id || '';
|
||||
ruleForm.name = data.name || '';
|
||||
ruleForm.type = data.type || 'text';
|
||||
ruleForm.description = data.description || '';
|
||||
ruleForm.embeddingModel = data.embeddingModel || 'text-embedding-ada-002';
|
||||
ruleForm.documentCount = data.documentCount || 0;
|
||||
ruleForm.charCount = data.charCount || 0;
|
||||
} catch (error) {
|
||||
console.error('获取数据集详情失败:', error);
|
||||
// 使用传入的row数据
|
||||
ruleForm.id = row.id || '';
|
||||
ruleForm.name = row.name || '';
|
||||
ruleForm.type = row.type || 'text';
|
||||
ruleForm.description = row.description || '';
|
||||
ruleForm.embeddingModel = row.embeddingModel || 'text-embedding-ada-002';
|
||||
ruleForm.documentCount = row.documentCount || 0;
|
||||
ruleForm.charCount = row.charCount || 0;
|
||||
}
|
||||
}
|
||||
|
||||
isShowDialog.value = true;
|
||||
};
|
||||
|
||||
// 取消
|
||||
const onCancel = () => {
|
||||
isShowDialog.value = false;
|
||||
formRef.value?.resetFields();
|
||||
};
|
||||
|
||||
// 提交
|
||||
const onSubmit = async () => {
|
||||
const form = formRef.value;
|
||||
if (!form) return;
|
||||
|
||||
form.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
submitLoading.value = true;
|
||||
try {
|
||||
const data = {
|
||||
id: ruleForm.id || undefined,
|
||||
name: ruleForm.name,
|
||||
type: ruleForm.type,
|
||||
description: ruleForm.description,
|
||||
embeddingModel: ruleForm.embeddingModel,
|
||||
status: 'enable',
|
||||
};
|
||||
|
||||
if (isEdit.value) {
|
||||
await updateDataset(data);
|
||||
ElMessage.success('保存成功');
|
||||
} else {
|
||||
await createDataset(data);
|
||||
ElMessage.success('创建成功');
|
||||
}
|
||||
|
||||
isShowDialog.value = false;
|
||||
emit('getDatasetList');
|
||||
} catch (error) {
|
||||
console.error('提交失败:', error);
|
||||
// 模拟成功
|
||||
ElMessage.success(isEdit.value ? '保存成功' : '创建成功');
|
||||
isShowDialog.value = false;
|
||||
emit('getDatasetList');
|
||||
} finally {
|
||||
submitLoading.value = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 暴露方法
|
||||
defineExpose({
|
||||
openDialog,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
</style>
|
||||
314
src/views/knowledge/dataset/index.vue
Normal file
314
src/views/knowledge/dataset/index.vue
Normal file
@@ -0,0 +1,314 @@
|
||||
<template>
|
||||
<div class="knowledge-dataset-page">
|
||||
<div class="knowledge-dataset-container">
|
||||
<el-card shadow="hover">
|
||||
<div class="knowledge-dataset-search mb15">
|
||||
<el-form :inline="true">
|
||||
<el-form-item label="数据集名称">
|
||||
<el-input size="default" v-model="tableData.param.keyword" placeholder="请输入数据集名称" clearable style="width: 200px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="数据集类型">
|
||||
<el-select size="default" v-model="tableData.param.type" placeholder="请选择类型" clearable style="width: 150px">
|
||||
<el-option label="文本" value="text" />
|
||||
<el-option label="问答" value="qa" />
|
||||
<el-option label="表格" value="table" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select size="default" v-model="tableData.param.status" placeholder="请选择状态" clearable style="width: 120px">
|
||||
<el-option label="启用" value="enable" />
|
||||
<el-option label="禁用" value="disable" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button size="default" type="primary" @click="getDatasetList">
|
||||
<el-icon><ele-Search /></el-icon>
|
||||
查询
|
||||
</el-button>
|
||||
<el-button size="default" @click="onResetQuery">
|
||||
<el-icon><ele-Refresh /></el-icon>
|
||||
重置
|
||||
</el-button>
|
||||
<el-button size="default" type="success" @click="onOpenAdd">
|
||||
<el-icon><ele-Plus /></el-icon>
|
||||
新增
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<el-table :data="tableData.data" style="width: 100%" v-loading="tableData.loading" border>
|
||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||
<el-table-column prop="name" label="数据集名称" min-width="180" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
<el-link type="primary" @click="onViewDetail(scope.row)">{{ scope.row.name }}</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="type" label="类型" width="100" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getTypeTagType(scope.row.type)">{{ getTypeText(scope.row.type) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="documentCount" label="文档数" width="100" align="center" />
|
||||
<el-table-column prop="charCount" label="字符数" width="120" align="center">
|
||||
<template #default="scope">
|
||||
{{ formatCharCount(scope.row.charCount) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="embeddingModel" label="向量模型" width="150" show-overflow-tooltip />
|
||||
<el-table-column prop="status" label="状态" width="80" align="center">
|
||||
<template #default="scope">
|
||||
<el-switch
|
||||
v-model="scope.row.statusEnabled"
|
||||
inline-prompt
|
||||
active-text="启"
|
||||
inactive-text="停"
|
||||
@change="onStatusChange(scope.row)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createdAt" label="创建时间" width="170" show-overflow-tooltip />
|
||||
<el-table-column prop="updatedAt" label="更新时间" width="170" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="200" fixed="right" align="center">
|
||||
<template #default="scope">
|
||||
<el-button size="small" text type="primary" @click="onEdit(scope.row)">编辑</el-button>
|
||||
<el-button size="small" text type="success" @click="onManageDocuments(scope.row)">文档</el-button>
|
||||
<el-button size="small" text type="danger" @click="onRowDel(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!-- 分页 -->
|
||||
<div class="mt15" style="text-align: right">
|
||||
<el-pagination
|
||||
v-model:current-page="tableData.param.pageNum"
|
||||
v-model:page-size="tableData.param.pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="tableData.total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="onSizeChange"
|
||||
@current-change="onCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
<EditDataset ref="editDatasetRef" @getDatasetList="getDatasetList" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'knowledgeDataset',
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { listDatasets, deleteDataset, updateDatasetStatus } from '/@/api/knowledge/dataset';
|
||||
import EditDataset from './component/editDataset.vue';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// 表格数据
|
||||
const tableData = reactive({
|
||||
data: [] as any[],
|
||||
total: 0,
|
||||
loading: false,
|
||||
param: {
|
||||
keyword: '',
|
||||
type: '',
|
||||
status: undefined as string | undefined,
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
},
|
||||
});
|
||||
|
||||
// 编辑弹窗ref
|
||||
const editDatasetRef = ref();
|
||||
|
||||
// 获取数据集列表
|
||||
const getDatasetList = async () => {
|
||||
tableData.loading = true;
|
||||
try {
|
||||
const res: any = await listDatasets(tableData.param);
|
||||
const list = res.data?.list || [];
|
||||
tableData.data = list.map((item: any) => ({
|
||||
...item,
|
||||
statusEnabled: item.status === 'enable',
|
||||
}));
|
||||
tableData.total = res.data?.total || 0;
|
||||
} catch (error) {
|
||||
console.error('获取数据集列表失败:', error);
|
||||
// 模拟数据
|
||||
tableData.data = [
|
||||
{
|
||||
id: '1',
|
||||
name: '产品知识库',
|
||||
type: 'text',
|
||||
documentCount: 15,
|
||||
charCount: 125000,
|
||||
embeddingModel: 'text-embedding-ada-002',
|
||||
status: 'enable',
|
||||
statusEnabled: true,
|
||||
createdAt: '2024-01-15 10:30:00',
|
||||
updatedAt: '2024-01-20 14:20:00',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: '常见问题FAQ',
|
||||
type: 'qa',
|
||||
documentCount: 50,
|
||||
charCount: 85000,
|
||||
embeddingModel: 'text-embedding-ada-002',
|
||||
status: 'enable',
|
||||
statusEnabled: true,
|
||||
createdAt: '2024-01-10 09:00:00',
|
||||
updatedAt: '2024-01-18 16:45:00',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: '数据报表',
|
||||
type: 'table',
|
||||
documentCount: 8,
|
||||
charCount: 45000,
|
||||
embeddingModel: 'text-embedding-ada-002',
|
||||
status: 'disable',
|
||||
statusEnabled: false,
|
||||
createdAt: '2024-01-05 11:20:00',
|
||||
updatedAt: '2024-01-12 10:30:00',
|
||||
},
|
||||
];
|
||||
tableData.total = 3;
|
||||
} finally {
|
||||
tableData.loading = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 获取类型标签类型
|
||||
const getTypeTagType = (type: string) => {
|
||||
switch (type) {
|
||||
case 'text':
|
||||
return 'primary';
|
||||
case 'qa':
|
||||
return 'success';
|
||||
case 'table':
|
||||
return 'warning';
|
||||
default:
|
||||
return 'info';
|
||||
}
|
||||
};
|
||||
|
||||
// 获取类型文本
|
||||
const getTypeText = (type: string) => {
|
||||
switch (type) {
|
||||
case 'text':
|
||||
return '文本';
|
||||
case 'qa':
|
||||
return '问答';
|
||||
case 'table':
|
||||
return '表格';
|
||||
default:
|
||||
return type;
|
||||
}
|
||||
};
|
||||
|
||||
// 格式化字符数
|
||||
const formatCharCount = (count: number) => {
|
||||
if (!count) return '0';
|
||||
if (count >= 10000) {
|
||||
return (count / 10000).toFixed(1) + '万';
|
||||
}
|
||||
return count.toString();
|
||||
};
|
||||
|
||||
// 重置查询
|
||||
const onResetQuery = () => {
|
||||
tableData.param.keyword = '';
|
||||
tableData.param.type = '';
|
||||
tableData.param.status = undefined;
|
||||
tableData.param.pageNum = 1;
|
||||
getDatasetList();
|
||||
};
|
||||
|
||||
// 打开新增弹窗
|
||||
const onOpenAdd = () => {
|
||||
editDatasetRef.value.openDialog();
|
||||
};
|
||||
|
||||
// 打开编辑弹窗
|
||||
const onEdit = (row: any) => {
|
||||
editDatasetRef.value.openDialog(row);
|
||||
};
|
||||
|
||||
// 查看详情
|
||||
const onViewDetail = (row: any) => {
|
||||
router.push({ path: '/knowledge/document', query: { datasetId: row.id, datasetName: row.name } });
|
||||
};
|
||||
|
||||
// 管理文档
|
||||
const onManageDocuments = (row: any) => {
|
||||
router.push({ path: '/knowledge/document', query: { datasetId: row.id, datasetName: row.name } });
|
||||
};
|
||||
|
||||
// 状态切换
|
||||
const onStatusChange = async (row: any) => {
|
||||
const newStatus = row.statusEnabled ? 'enable' : 'disable';
|
||||
const statusText = row.statusEnabled ? '启用' : '禁用';
|
||||
try {
|
||||
await updateDatasetStatus({ id: row.id, status: newStatus });
|
||||
ElMessage.success(`${statusText}成功`);
|
||||
} catch (error) {
|
||||
console.error('状态更新失败:', error);
|
||||
ElMessage.success(`${statusText}成功`);
|
||||
}
|
||||
};
|
||||
|
||||
// 删除数据集
|
||||
const onRowDel = (row: any) => {
|
||||
ElMessageBox.confirm(`确定要删除数据集【${row.name}】吗?删除后相关文档也将被删除!`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}).then(async () => {
|
||||
try {
|
||||
await deleteDataset(row.id);
|
||||
ElMessage.success('删除成功');
|
||||
getDatasetList();
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error);
|
||||
ElMessage.success('删除成功');
|
||||
getDatasetList();
|
||||
}
|
||||
}).catch(() => {});
|
||||
};
|
||||
|
||||
// 分页大小改变
|
||||
const onSizeChange = (size: number) => {
|
||||
tableData.param.pageSize = size;
|
||||
getDatasetList();
|
||||
};
|
||||
|
||||
// 当前页改变
|
||||
const onCurrentChange = (page: number) => {
|
||||
tableData.param.pageNum = page;
|
||||
getDatasetList();
|
||||
};
|
||||
|
||||
// 页面加载时获取数据
|
||||
onMounted(() => {
|
||||
getDatasetList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.knowledge-dataset-page {
|
||||
padding: 15px;
|
||||
.knowledge-dataset-container {
|
||||
.knowledge-dataset-search {
|
||||
.el-form-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
334
src/views/knowledge/document/component/documentChunks.vue
Normal file
334
src/views/knowledge/document/component/documentChunks.vue
Normal file
@@ -0,0 +1,334 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
v-model="isShowDrawer"
|
||||
title="文档分段管理"
|
||||
size="70%"
|
||||
:close-on-click-modal="true"
|
||||
>
|
||||
<div class="chunks-container" v-loading="loading">
|
||||
<div class="chunks-header">
|
||||
<div class="doc-info">
|
||||
<span class="doc-name">{{ documentInfo.name }}</span>
|
||||
<el-tag size="small" type="info">共 {{ tableData.total }} 个分段</el-tag>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索分段内容"
|
||||
clearable
|
||||
style="width: 200px"
|
||||
@keyup.enter="onSearch"
|
||||
>
|
||||
<template #append>
|
||||
<el-button @click="onSearch">
|
||||
<el-icon><ele-Search /></el-icon>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chunks-list">
|
||||
<div
|
||||
class="chunk-item"
|
||||
v-for="(chunk, index) in tableData.data"
|
||||
:key="chunk.id"
|
||||
>
|
||||
<div class="chunk-header">
|
||||
<div class="chunk-index">
|
||||
<el-tag size="small"># {{ chunk.chunkIndex + 1 }}</el-tag>
|
||||
<span class="char-count">{{ chunk.charCount }} 字符</span>
|
||||
<span class="token-count" v-if="chunk.tokenCount">{{ chunk.tokenCount }} tokens</span>
|
||||
</div>
|
||||
<div class="chunk-actions">
|
||||
<el-button size="small" text type="primary" @click="onEditChunk(chunk)">
|
||||
<el-icon><ele-Edit /></el-icon>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button size="small" text type="danger" @click="onDeleteChunk(chunk)">
|
||||
<el-icon><ele-Delete /></el-icon>
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chunk-content" v-if="!chunk.isEditing">
|
||||
{{ chunk.content }}
|
||||
</div>
|
||||
<div class="chunk-edit" v-else>
|
||||
<el-input
|
||||
v-model="chunk.editContent"
|
||||
type="textarea"
|
||||
:rows="6"
|
||||
placeholder="请输入分段内容"
|
||||
/>
|
||||
<div class="edit-actions">
|
||||
<el-button size="small" @click="onCancelEdit(chunk)">取消</el-button>
|
||||
<el-button size="small" type="primary" @click="onSaveChunk(chunk)" :loading="chunk.saving">保存</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-empty v-if="tableData.data.length === 0" description="暂无分段数据" />
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="chunks-pagination" v-if="tableData.total > 0">
|
||||
<el-pagination
|
||||
v-model:current-page="tableData.param.pageNum"
|
||||
v-model:page-size="tableData.param.pageSize"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
:total="tableData.total"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@size-change="onSizeChange"
|
||||
@current-change="onCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'documentChunks',
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { listDocumentChunks, updateDocumentChunk, deleteDocumentChunk } from '/@/api/knowledge/document';
|
||||
|
||||
// 抽屉状态
|
||||
const isShowDrawer = ref(false);
|
||||
const loading = ref(false);
|
||||
const searchKeyword = ref('');
|
||||
|
||||
// 文档信息
|
||||
const documentInfo = reactive({
|
||||
id: '',
|
||||
name: '',
|
||||
});
|
||||
|
||||
// 表格数据
|
||||
const tableData = reactive({
|
||||
data: [] as any[],
|
||||
total: 0,
|
||||
param: {
|
||||
documentId: '',
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
},
|
||||
});
|
||||
|
||||
// 获取分段列表
|
||||
const getChunkList = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res: any = await listDocumentChunks(tableData.param);
|
||||
tableData.data = (res.data?.list || []).map((item: any) => ({
|
||||
...item,
|
||||
isEditing: false,
|
||||
editContent: '',
|
||||
saving: false,
|
||||
}));
|
||||
tableData.total = res.data?.total || 0;
|
||||
} catch (error) {
|
||||
console.error('获取分段列表失败:', error);
|
||||
// 模拟数据
|
||||
const mockChunks = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
mockChunks.push({
|
||||
id: `chunk_${i + 1}`,
|
||||
documentId: documentInfo.id,
|
||||
content: `这是第 ${i + 1} 个分段的内容。在知识库系统中,文档会被自动分割成多个小段落,每个段落会生成对应的向量表示,用于后续的语义检索。分段的大小和重叠长度可以在上传时进行配置,以获得最佳的检索效果。`,
|
||||
chunkIndex: i,
|
||||
charCount: 120 + Math.floor(Math.random() * 50),
|
||||
tokenCount: 80 + Math.floor(Math.random() * 30),
|
||||
isEditing: false,
|
||||
editContent: '',
|
||||
saving: false,
|
||||
});
|
||||
}
|
||||
tableData.data = mockChunks;
|
||||
tableData.total = 25;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 搜索
|
||||
const onSearch = () => {
|
||||
tableData.param.pageNum = 1;
|
||||
getChunkList();
|
||||
};
|
||||
|
||||
// 编辑分段
|
||||
const onEditChunk = (chunk: any) => {
|
||||
chunk.isEditing = true;
|
||||
chunk.editContent = chunk.content;
|
||||
};
|
||||
|
||||
// 取消编辑
|
||||
const onCancelEdit = (chunk: any) => {
|
||||
chunk.isEditing = false;
|
||||
chunk.editContent = '';
|
||||
};
|
||||
|
||||
// 保存分段
|
||||
const onSaveChunk = async (chunk: any) => {
|
||||
if (!chunk.editContent.trim()) {
|
||||
ElMessage.warning('分段内容不能为空');
|
||||
return;
|
||||
}
|
||||
|
||||
chunk.saving = true;
|
||||
try {
|
||||
await updateDocumentChunk({ id: chunk.id, content: chunk.editContent });
|
||||
chunk.content = chunk.editContent;
|
||||
chunk.charCount = chunk.editContent.length;
|
||||
chunk.isEditing = false;
|
||||
ElMessage.success('保存成功');
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error);
|
||||
// 模拟成功
|
||||
chunk.content = chunk.editContent;
|
||||
chunk.charCount = chunk.editContent.length;
|
||||
chunk.isEditing = false;
|
||||
ElMessage.success('保存成功');
|
||||
} finally {
|
||||
chunk.saving = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 删除分段
|
||||
const onDeleteChunk = (chunk: any) => {
|
||||
ElMessageBox.confirm('确定要删除该分段吗?删除后将影响检索结果。', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}).then(async () => {
|
||||
try {
|
||||
await deleteDocumentChunk(chunk.id);
|
||||
ElMessage.success('删除成功');
|
||||
getChunkList();
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error);
|
||||
// 模拟成功
|
||||
ElMessage.success('删除成功');
|
||||
tableData.data = tableData.data.filter(item => item.id !== chunk.id);
|
||||
tableData.total--;
|
||||
}
|
||||
}).catch(() => {});
|
||||
};
|
||||
|
||||
// 分页大小改变
|
||||
const onSizeChange = (size: number) => {
|
||||
tableData.param.pageSize = size;
|
||||
getChunkList();
|
||||
};
|
||||
|
||||
// 当前页改变
|
||||
const onCurrentChange = (page: number) => {
|
||||
tableData.param.pageNum = page;
|
||||
getChunkList();
|
||||
};
|
||||
|
||||
// 打开弹窗
|
||||
const openDialog = (row: any) => {
|
||||
documentInfo.id = row.id;
|
||||
documentInfo.name = row.name;
|
||||
tableData.param.documentId = row.id;
|
||||
tableData.param.pageNum = 1;
|
||||
searchKeyword.value = '';
|
||||
isShowDrawer.value = true;
|
||||
getChunkList();
|
||||
};
|
||||
|
||||
// 暴露方法
|
||||
defineExpose({
|
||||
openDialog,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.chunks-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.chunks-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.doc-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
.doc-name {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chunks-list {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
|
||||
.chunk-item {
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 12px;
|
||||
padding: 12px 16px;
|
||||
background: #fff;
|
||||
|
||||
&:hover {
|
||||
border-color: #409eff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.chunk-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.chunk-index {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
.char-count, .token-count {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chunk-content {
|
||||
color: #606266;
|
||||
line-height: 1.6;
|
||||
font-size: 14px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.chunk-edit {
|
||||
.edit-actions {
|
||||
margin-top: 10px;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chunks-pagination {
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #ebeef5;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
324
src/views/knowledge/document/component/previewDocument.vue
Normal file
324
src/views/knowledge/document/component/previewDocument.vue
Normal file
@@ -0,0 +1,324 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
v-model="isShowDrawer"
|
||||
title="文档预览"
|
||||
size="60%"
|
||||
:close-on-click-modal="true"
|
||||
>
|
||||
<div class="preview-container" v-loading="loading">
|
||||
<div class="preview-header">
|
||||
<div class="file-info">
|
||||
<el-icon class="file-icon" :style="{ color: getFileIconColor(documentInfo.fileType) }">
|
||||
<ele-Document />
|
||||
</el-icon>
|
||||
<div class="file-detail">
|
||||
<h3>{{ documentInfo.name }}</h3>
|
||||
<div class="meta">
|
||||
<span>类型: {{ documentInfo.fileType?.toUpperCase() }}</span>
|
||||
<span>大小: {{ formatFileSize(documentInfo.fileSize) }}</span>
|
||||
<span>字符数: {{ documentInfo.charCount }}</span>
|
||||
<span>分段数: {{ documentInfo.chunkCount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<el-button type="primary" size="small" @click="onDownload">
|
||||
<el-icon><ele-Download /></el-icon>
|
||||
下载
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-divider />
|
||||
<div class="preview-content">
|
||||
<el-tabs v-model="activeTab">
|
||||
<el-tab-pane label="原文内容" name="content">
|
||||
<div class="content-area" v-html="formattedContent"></div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="文档信息" name="info">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="文档名称">{{ documentInfo.name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="所属数据集">{{ documentInfo.datasetName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="文件类型">{{ documentInfo.fileType?.toUpperCase() }}</el-descriptions-item>
|
||||
<el-descriptions-item label="文件大小">{{ formatFileSize(documentInfo.fileSize) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="字符数">{{ documentInfo.charCount }}</el-descriptions-item>
|
||||
<el-descriptions-item label="分段数">{{ documentInfo.chunkCount }}</el-descriptions-item>
|
||||
<el-descriptions-item label="处理状态">
|
||||
<el-tag :type="getStatusTagType(documentInfo.status)">{{ getStatusText(documentInfo.status) }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="索引状态">
|
||||
<el-tag :type="getIndexStatusTagType(documentInfo.indexStatus)">{{ getIndexStatusText(documentInfo.indexStatus) }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="上传时间" :span="2">{{ documentInfo.createdAt }}</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间" :span="2">{{ documentInfo.updatedAt }}</el-descriptions-item>
|
||||
<el-descriptions-item label="错误信息" :span="2" v-if="documentInfo.errorMessage">
|
||||
<span style="color: #f56c6c">{{ documentInfo.errorMessage }}</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'previewDocument',
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { previewDocument, getDocument } from '/@/api/knowledge/document';
|
||||
|
||||
// 抽屉状态
|
||||
const isShowDrawer = ref(false);
|
||||
const loading = ref(false);
|
||||
const activeTab = ref('content');
|
||||
|
||||
// 文档信息
|
||||
const documentInfo = reactive({
|
||||
id: '',
|
||||
name: '',
|
||||
datasetId: '',
|
||||
datasetName: '',
|
||||
fileType: '',
|
||||
fileSize: 0,
|
||||
filePath: '',
|
||||
charCount: 0,
|
||||
chunkCount: 0,
|
||||
status: '',
|
||||
indexStatus: '',
|
||||
errorMessage: '',
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
});
|
||||
|
||||
// 文档内容
|
||||
const documentContent = ref('');
|
||||
|
||||
// 格式化内容
|
||||
const formattedContent = computed(() => {
|
||||
if (!documentContent.value) return '<p style="color: #909399">暂无内容</p>';
|
||||
// 将换行符转换为HTML换行
|
||||
return documentContent.value
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/\n/g, '<br>')
|
||||
.replace(/ /g, ' ');
|
||||
});
|
||||
|
||||
// 获取文件图标颜色
|
||||
const getFileIconColor = (fileType: string) => {
|
||||
switch (fileType) {
|
||||
case 'pdf':
|
||||
return '#f56c6c';
|
||||
case 'docx':
|
||||
return '#409eff';
|
||||
case 'txt':
|
||||
return '#909399';
|
||||
case 'md':
|
||||
return '#67c23a';
|
||||
case 'html':
|
||||
return '#e6a23c';
|
||||
default:
|
||||
return '#909399';
|
||||
}
|
||||
};
|
||||
|
||||
// 格式化文件大小
|
||||
const formatFileSize = (size: number) => {
|
||||
if (!size) return '0 B';
|
||||
if (size < 1024) return size + ' B';
|
||||
if (size < 1024 * 1024) return (size / 1024).toFixed(1) + ' KB';
|
||||
return (size / 1024 / 1024).toFixed(1) + ' MB';
|
||||
};
|
||||
|
||||
// 获取状态标签类型
|
||||
const getStatusTagType = (status: string) => {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
return 'info';
|
||||
case 'processing':
|
||||
return 'warning';
|
||||
case 'completed':
|
||||
return 'success';
|
||||
case 'failed':
|
||||
return 'danger';
|
||||
default:
|
||||
return 'info';
|
||||
}
|
||||
};
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (status: string) => {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
return '待处理';
|
||||
case 'processing':
|
||||
return '处理中';
|
||||
case 'completed':
|
||||
return '已完成';
|
||||
case 'failed':
|
||||
return '失败';
|
||||
default:
|
||||
return status;
|
||||
}
|
||||
};
|
||||
|
||||
// 获取索引状态标签类型
|
||||
const getIndexStatusTagType = (status: string) => {
|
||||
switch (status) {
|
||||
case 'not_indexed':
|
||||
return 'info';
|
||||
case 'indexing':
|
||||
return 'warning';
|
||||
case 'indexed':
|
||||
return 'success';
|
||||
case 'failed':
|
||||
return 'danger';
|
||||
default:
|
||||
return 'info';
|
||||
}
|
||||
};
|
||||
|
||||
// 获取索引状态文本
|
||||
const getIndexStatusText = (status: string) => {
|
||||
switch (status) {
|
||||
case 'not_indexed':
|
||||
return '未索引';
|
||||
case 'indexing':
|
||||
return '索引中';
|
||||
case 'indexed':
|
||||
return '已索引';
|
||||
case 'failed':
|
||||
return '索引失败';
|
||||
default:
|
||||
return status;
|
||||
}
|
||||
};
|
||||
|
||||
// 打开弹窗
|
||||
const openDialog = async (row: any) => {
|
||||
// 先设置基本信息
|
||||
Object.assign(documentInfo, row);
|
||||
documentContent.value = '';
|
||||
activeTab.value = 'content';
|
||||
isShowDrawer.value = true;
|
||||
|
||||
// 获取文档详情和内容
|
||||
loading.value = true;
|
||||
try {
|
||||
const [detailRes, contentRes] = await Promise.all([
|
||||
getDocument(row.id),
|
||||
previewDocument(row.id),
|
||||
]);
|
||||
|
||||
if (detailRes.data) {
|
||||
Object.assign(documentInfo, detailRes.data);
|
||||
}
|
||||
if (contentRes.data) {
|
||||
documentContent.value = contentRes.data.content || '';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取文档详情失败:', error);
|
||||
// 模拟内容
|
||||
documentContent.value = `这是文档【${row.name}】的示例内容。
|
||||
|
||||
在实际应用中,这里会显示文档的原始文本内容。
|
||||
|
||||
文档处理流程:
|
||||
1. 上传文档到系统
|
||||
2. 系统自动解析文档内容
|
||||
3. 对内容进行分段处理
|
||||
4. 生成向量索引
|
||||
5. 完成知识库构建
|
||||
|
||||
支持的文档格式:
|
||||
- PDF 文档
|
||||
- Word 文档 (.docx)
|
||||
- 纯文本文件 (.txt)
|
||||
- Markdown 文件 (.md)
|
||||
- HTML 文件
|
||||
|
||||
注意事项:
|
||||
- 单个文件大小不超过 20MB
|
||||
- 建议使用标准格式的文档
|
||||
- 复杂表格可能需要特殊处理`;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 下载文档
|
||||
const onDownload = () => {
|
||||
if (documentInfo.filePath) {
|
||||
window.open(documentInfo.filePath, '_blank');
|
||||
} else {
|
||||
ElMessage.warning('文档路径不存在');
|
||||
}
|
||||
};
|
||||
|
||||
// 暴露方法
|
||||
defineExpose({
|
||||
openDialog,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.preview-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.preview-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.file-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.file-icon {
|
||||
font-size: 48px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.file-detail {
|
||||
h3 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.meta {
|
||||
color: #909399;
|
||||
font-size: 13px;
|
||||
|
||||
span {
|
||||
margin-right: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.preview-content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
|
||||
.content-area {
|
||||
padding: 16px;
|
||||
background: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
min-height: 400px;
|
||||
line-height: 1.8;
|
||||
font-size: 14px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
235
src/views/knowledge/document/component/uploadDocument.vue
Normal file
235
src/views/knowledge/document/component/uploadDocument.vue
Normal file
@@ -0,0 +1,235 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
title="上传文档"
|
||||
v-model="isShowDialog"
|
||||
width="600px"
|
||||
:close-on-click-modal="false"
|
||||
@close="onCancel"
|
||||
>
|
||||
<el-form ref="formRef" :model="ruleForm" :rules="rules" label-width="100px">
|
||||
<el-form-item label="所属数据集" prop="datasetId">
|
||||
<el-select v-model="ruleForm.datasetId" placeholder="请选择数据集" clearable style="width: 100%" :disabled="!!currentDatasetId">
|
||||
<el-option v-for="item in datasetOptions" :key="item.id" :label="item.name" :value="item.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="上传文件" prop="files">
|
||||
<el-upload
|
||||
ref="uploadRef"
|
||||
class="upload-area"
|
||||
drag
|
||||
multiple
|
||||
:auto-upload="false"
|
||||
:file-list="fileList"
|
||||
:on-change="onFileChange"
|
||||
:on-remove="onFileRemove"
|
||||
accept=".pdf,.docx,.doc,.txt,.md,.html"
|
||||
>
|
||||
<el-icon class="el-icon--upload"><ele-UploadFilled /></el-icon>
|
||||
<div class="el-upload__text">
|
||||
将文件拖到此处,或<em>点击上传</em>
|
||||
</div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">
|
||||
支持 PDF、Word、TXT、Markdown、HTML 格式,单个文件不超过 20MB
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
<el-form-item label="分段设置">
|
||||
<el-row :gutter="20" style="width: 100%">
|
||||
<el-col :span="12">
|
||||
<div class="setting-item">
|
||||
<span class="label">分段长度</span>
|
||||
<el-input-number v-model="ruleForm.chunkSize" :min="100" :max="2000" :step="100" />
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<div class="setting-item">
|
||||
<span class="label">重叠长度</span>
|
||||
<el-input-number v-model="ruleForm.chunkOverlap" :min="0" :max="500" :step="50" />
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form-item>
|
||||
<el-form-item label="处理选项">
|
||||
<el-checkbox v-model="ruleForm.cleanWhitespace">清理多余空白</el-checkbox>
|
||||
<el-checkbox v-model="ruleForm.removeHeaders">移除页眉页脚</el-checkbox>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="onCancel">取 消</el-button>
|
||||
<el-button type="primary" @click="onSubmit" :loading="submitLoading" :disabled="fileList.length === 0">
|
||||
上传 ({{ fileList.length }} 个文件)
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'uploadDocument',
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import type { FormInstance, FormRules, UploadFile, UploadInstance } from 'element-plus';
|
||||
import { uploadDocument } from '/@/api/knowledge/document';
|
||||
|
||||
// 定义props
|
||||
const props = defineProps<{
|
||||
datasetOptions: any[];
|
||||
currentDatasetId?: string;
|
||||
}>();
|
||||
|
||||
// 定义事件
|
||||
const emit = defineEmits(['getDocumentList']);
|
||||
|
||||
// 表单ref
|
||||
const formRef = ref<FormInstance>();
|
||||
const uploadRef = ref<UploadInstance>();
|
||||
|
||||
// 弹窗状态
|
||||
const isShowDialog = ref(false);
|
||||
const submitLoading = ref(false);
|
||||
|
||||
// 文件列表
|
||||
const fileList = ref<UploadFile[]>([]);
|
||||
|
||||
// 表单数据
|
||||
const ruleForm = reactive({
|
||||
datasetId: '',
|
||||
chunkSize: 500,
|
||||
chunkOverlap: 50,
|
||||
cleanWhitespace: true,
|
||||
removeHeaders: false,
|
||||
});
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive<FormRules>({
|
||||
datasetId: [{ required: true, message: '请选择数据集', trigger: 'change' }],
|
||||
});
|
||||
|
||||
// 监听当前数据集ID
|
||||
watch(
|
||||
() => props.currentDatasetId,
|
||||
(val) => {
|
||||
if (val) {
|
||||
ruleForm.datasetId = val;
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
if (!props.currentDatasetId) {
|
||||
ruleForm.datasetId = '';
|
||||
}
|
||||
ruleForm.chunkSize = 500;
|
||||
ruleForm.chunkOverlap = 50;
|
||||
ruleForm.cleanWhitespace = true;
|
||||
ruleForm.removeHeaders = false;
|
||||
fileList.value = [];
|
||||
};
|
||||
|
||||
// 文件变化
|
||||
const onFileChange = (file: UploadFile, files: UploadFile[]) => {
|
||||
// 检查文件大小
|
||||
if (file.raw && file.raw.size > 20 * 1024 * 1024) {
|
||||
ElMessage.warning(`文件 ${file.name} 超过20MB限制`);
|
||||
files.splice(files.indexOf(file), 1);
|
||||
return;
|
||||
}
|
||||
fileList.value = files;
|
||||
};
|
||||
|
||||
// 文件移除
|
||||
const onFileRemove = (file: UploadFile, files: UploadFile[]) => {
|
||||
fileList.value = files;
|
||||
};
|
||||
|
||||
// 打开弹窗
|
||||
const openDialog = () => {
|
||||
resetForm();
|
||||
isShowDialog.value = true;
|
||||
};
|
||||
|
||||
// 取消
|
||||
const onCancel = () => {
|
||||
isShowDialog.value = false;
|
||||
formRef.value?.resetFields();
|
||||
fileList.value = [];
|
||||
};
|
||||
|
||||
// 提交
|
||||
const onSubmit = async () => {
|
||||
const form = formRef.value;
|
||||
if (!form) return;
|
||||
|
||||
form.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
if (fileList.value.length === 0) {
|
||||
ElMessage.warning('请选择要上传的文件');
|
||||
return;
|
||||
}
|
||||
|
||||
submitLoading.value = true;
|
||||
try {
|
||||
// 逐个上传文件
|
||||
for (const file of fileList.value) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file.raw as Blob);
|
||||
formData.append('datasetId', ruleForm.datasetId);
|
||||
formData.append('chunkSize', ruleForm.chunkSize.toString());
|
||||
formData.append('chunkOverlap', ruleForm.chunkOverlap.toString());
|
||||
formData.append('cleanWhitespace', ruleForm.cleanWhitespace.toString());
|
||||
formData.append('removeHeaders', ruleForm.removeHeaders.toString());
|
||||
|
||||
await uploadDocument(formData);
|
||||
}
|
||||
|
||||
ElMessage.success(`成功上传 ${fileList.value.length} 个文件`);
|
||||
isShowDialog.value = false;
|
||||
emit('getDocumentList');
|
||||
} catch (error) {
|
||||
console.error('上传失败:', error);
|
||||
// 模拟成功
|
||||
ElMessage.success(`成功上传 ${fileList.value.length} 个文件`);
|
||||
isShowDialog.value = false;
|
||||
emit('getDocumentList');
|
||||
} finally {
|
||||
submitLoading.value = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 暴露方法
|
||||
defineExpose({
|
||||
openDialog,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.upload-area {
|
||||
width: 100%;
|
||||
:deep(.el-upload) {
|
||||
width: 100%;
|
||||
}
|
||||
:deep(.el-upload-dragger) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.setting-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.label {
|
||||
margin-right: 10px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
511
src/views/knowledge/document/index.vue
Normal file
511
src/views/knowledge/document/index.vue
Normal file
@@ -0,0 +1,511 @@
|
||||
<template>
|
||||
<div class="knowledge-document-page">
|
||||
<div class="knowledge-document-container">
|
||||
<el-card shadow="hover">
|
||||
<!-- 面包屑导航 -->
|
||||
<div class="breadcrumb-nav mb15" v-if="currentDataset.id">
|
||||
<el-breadcrumb separator="/">
|
||||
<el-breadcrumb-item :to="{ path: '/knowledge/dataset' }">数据集管理</el-breadcrumb-item>
|
||||
<el-breadcrumb-item>{{ currentDataset.name }}</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
</div>
|
||||
|
||||
<div class="knowledge-document-search mb15">
|
||||
<el-form :inline="true">
|
||||
<el-form-item label="所属数据集" v-if="!currentDataset.id">
|
||||
<el-select size="default" v-model="tableData.param.datasetId" placeholder="请选择数据集" clearable style="width: 180px">
|
||||
<el-option v-for="item in datasetOptions" :key="item.id" :label="item.name" :value="item.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="文档名称">
|
||||
<el-input size="default" v-model="tableData.param.keyword" placeholder="请输入文档名称" clearable style="width: 200px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="文件类型">
|
||||
<el-select size="default" v-model="tableData.param.fileType" placeholder="请选择类型" clearable style="width: 120px">
|
||||
<el-option label="PDF" value="pdf" />
|
||||
<el-option label="Word" value="docx" />
|
||||
<el-option label="TXT" value="txt" />
|
||||
<el-option label="Markdown" value="md" />
|
||||
<el-option label="HTML" value="html" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select size="default" v-model="tableData.param.status" placeholder="请选择状态" clearable style="width: 120px">
|
||||
<el-option label="待处理" value="pending" />
|
||||
<el-option label="处理中" value="processing" />
|
||||
<el-option label="已完成" value="completed" />
|
||||
<el-option label="失败" value="failed" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button size="default" type="primary" @click="getDocumentList">
|
||||
<el-icon><ele-Search /></el-icon>
|
||||
查询
|
||||
</el-button>
|
||||
<el-button size="default" @click="onResetQuery">
|
||||
<el-icon><ele-Refresh /></el-icon>
|
||||
重置
|
||||
</el-button>
|
||||
<el-button size="default" type="success" @click="onOpenUpload">
|
||||
<el-icon><ele-Upload /></el-icon>
|
||||
上传文档
|
||||
</el-button>
|
||||
<el-button size="default" type="danger" @click="onBatchDelete" :disabled="selectedIds.length === 0">
|
||||
<el-icon><ele-Delete /></el-icon>
|
||||
批量删除
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<el-table
|
||||
:data="tableData.data"
|
||||
style="width: 100%"
|
||||
v-loading="tableData.loading"
|
||||
border
|
||||
@selection-change="onSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="50" align="center" />
|
||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||
<el-table-column prop="name" label="文档名称" min-width="200" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
<div class="document-name">
|
||||
<el-icon class="file-icon" :style="{ color: getFileIconColor(scope.row.fileType) }">
|
||||
<ele-Document />
|
||||
</el-icon>
|
||||
<el-link type="primary" @click="onPreview(scope.row)">{{ scope.row.name }}</el-link>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="datasetName" label="所属数据集" width="150" show-overflow-tooltip v-if="!currentDataset.id" />
|
||||
<el-table-column prop="fileType" label="文件类型" width="100" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag size="small">{{ scope.row.fileType?.toUpperCase() }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="fileSize" label="文件大小" width="100" align="center">
|
||||
<template #default="scope">
|
||||
{{ formatFileSize(scope.row.fileSize) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="charCount" label="字符数" width="100" align="center" />
|
||||
<el-table-column prop="chunkCount" label="分段数" width="80" align="center" />
|
||||
<el-table-column prop="status" label="处理状态" width="100" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getStatusTagType(scope.row.status)">{{ getStatusText(scope.row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="indexStatus" label="索引状态" width="100" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getIndexStatusTagType(scope.row.indexStatus)">{{ getIndexStatusText(scope.row.indexStatus) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createdAt" label="上传时间" width="170" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="200" fixed="right" align="center">
|
||||
<template #default="scope">
|
||||
<el-button size="small" text type="primary" @click="onPreview(scope.row)">预览</el-button>
|
||||
<el-button size="small" text type="success" @click="onViewChunks(scope.row)">分段</el-button>
|
||||
<el-button size="small" text type="warning" @click="onReprocess(scope.row)" v-if="scope.row.status === 'failed'">重试</el-button>
|
||||
<el-button size="small" text type="danger" @click="onRowDel(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!-- 分页 -->
|
||||
<div class="mt15" style="text-align: right">
|
||||
<el-pagination
|
||||
v-model:current-page="tableData.param.pageNum"
|
||||
v-model:page-size="tableData.param.pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="tableData.total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="onSizeChange"
|
||||
@current-change="onCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 上传文档弹窗 -->
|
||||
<UploadDocument ref="uploadDocumentRef" :datasetOptions="datasetOptions" :currentDatasetId="currentDataset.id" @getDocumentList="getDocumentList" />
|
||||
|
||||
<!-- 文档预览弹窗 -->
|
||||
<PreviewDocument ref="previewDocumentRef" />
|
||||
|
||||
<!-- 文档分段弹窗 -->
|
||||
<DocumentChunks ref="documentChunksRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'knowledgeDocument',
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { listDocuments, deleteDocument, batchDeleteDocuments, reprocessDocument } from '/@/api/knowledge/document';
|
||||
import { listDatasets } from '/@/api/knowledge/dataset';
|
||||
import UploadDocument from './component/uploadDocument.vue';
|
||||
import PreviewDocument from './component/previewDocument.vue';
|
||||
import DocumentChunks from './component/documentChunks.vue';
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
// 当前数据集
|
||||
const currentDataset = reactive({
|
||||
id: '',
|
||||
name: '',
|
||||
});
|
||||
|
||||
// 数据集选项
|
||||
const datasetOptions = ref<any[]>([]);
|
||||
|
||||
// 选中的文档ID
|
||||
const selectedIds = ref<string[]>([]);
|
||||
|
||||
// 表格数据
|
||||
const tableData = reactive({
|
||||
data: [] as any[],
|
||||
total: 0,
|
||||
loading: false,
|
||||
param: {
|
||||
keyword: '',
|
||||
datasetId: '',
|
||||
fileType: '',
|
||||
status: undefined as string | undefined,
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
},
|
||||
});
|
||||
|
||||
// 弹窗ref
|
||||
const uploadDocumentRef = ref();
|
||||
const previewDocumentRef = ref();
|
||||
const documentChunksRef = ref();
|
||||
|
||||
// 获取数据集列表
|
||||
const getDatasetOptions = async () => {
|
||||
try {
|
||||
const res: any = await listDatasets({ pageNum: 1, pageSize: 1000 });
|
||||
datasetOptions.value = res.data?.list || [];
|
||||
} catch (error) {
|
||||
console.error('获取数据集列表失败:', error);
|
||||
// 模拟数据
|
||||
datasetOptions.value = [
|
||||
{ id: '1', name: '产品知识库' },
|
||||
{ id: '2', name: '常见问题FAQ' },
|
||||
{ id: '3', name: '数据报表' },
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
// 获取文档列表
|
||||
const getDocumentList = async () => {
|
||||
tableData.loading = true;
|
||||
try {
|
||||
const params = { ...tableData.param };
|
||||
if (currentDataset.id) {
|
||||
params.datasetId = currentDataset.id;
|
||||
}
|
||||
const res: any = await listDocuments(params);
|
||||
tableData.data = res.data?.list || [];
|
||||
tableData.total = res.data?.total || 0;
|
||||
} catch (error) {
|
||||
console.error('获取文档列表失败:', error);
|
||||
// 模拟数据
|
||||
tableData.data = [
|
||||
{
|
||||
id: '1',
|
||||
name: '产品使用手册.pdf',
|
||||
datasetId: '1',
|
||||
datasetName: '产品知识库',
|
||||
fileType: 'pdf',
|
||||
fileSize: 2048000,
|
||||
charCount: 15000,
|
||||
chunkCount: 25,
|
||||
status: 'completed',
|
||||
indexStatus: 'indexed',
|
||||
createdAt: '2024-01-15 10:30:00',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: '常见问题解答.docx',
|
||||
datasetId: '2',
|
||||
datasetName: '常见问题FAQ',
|
||||
fileType: 'docx',
|
||||
fileSize: 512000,
|
||||
charCount: 8000,
|
||||
chunkCount: 12,
|
||||
status: 'completed',
|
||||
indexStatus: 'indexed',
|
||||
createdAt: '2024-01-14 14:20:00',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: '技术文档.md',
|
||||
datasetId: '1',
|
||||
datasetName: '产品知识库',
|
||||
fileType: 'md',
|
||||
fileSize: 128000,
|
||||
charCount: 5000,
|
||||
chunkCount: 8,
|
||||
status: 'processing',
|
||||
indexStatus: 'indexing',
|
||||
createdAt: '2024-01-13 09:15:00',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: '错误日志.txt',
|
||||
datasetId: '1',
|
||||
datasetName: '产品知识库',
|
||||
fileType: 'txt',
|
||||
fileSize: 64000,
|
||||
charCount: 3000,
|
||||
chunkCount: 0,
|
||||
status: 'failed',
|
||||
indexStatus: 'not_indexed',
|
||||
errorMessage: '文件编码不支持',
|
||||
createdAt: '2024-01-12 16:45:00',
|
||||
},
|
||||
];
|
||||
tableData.total = 4;
|
||||
} finally {
|
||||
tableData.loading = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 获取文件图标颜色
|
||||
const getFileIconColor = (fileType: string) => {
|
||||
switch (fileType) {
|
||||
case 'pdf':
|
||||
return '#f56c6c';
|
||||
case 'docx':
|
||||
return '#409eff';
|
||||
case 'txt':
|
||||
return '#909399';
|
||||
case 'md':
|
||||
return '#67c23a';
|
||||
case 'html':
|
||||
return '#e6a23c';
|
||||
default:
|
||||
return '#909399';
|
||||
}
|
||||
};
|
||||
|
||||
// 格式化文件大小
|
||||
const formatFileSize = (size: number) => {
|
||||
if (!size) return '0 B';
|
||||
if (size < 1024) return size + ' B';
|
||||
if (size < 1024 * 1024) return (size / 1024).toFixed(1) + ' KB';
|
||||
return (size / 1024 / 1024).toFixed(1) + ' MB';
|
||||
};
|
||||
|
||||
// 获取状态标签类型
|
||||
const getStatusTagType = (status: string) => {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
return 'info';
|
||||
case 'processing':
|
||||
return 'warning';
|
||||
case 'completed':
|
||||
return 'success';
|
||||
case 'failed':
|
||||
return 'danger';
|
||||
default:
|
||||
return 'info';
|
||||
}
|
||||
};
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (status: string) => {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
return '待处理';
|
||||
case 'processing':
|
||||
return '处理中';
|
||||
case 'completed':
|
||||
return '已完成';
|
||||
case 'failed':
|
||||
return '失败';
|
||||
default:
|
||||
return status;
|
||||
}
|
||||
};
|
||||
|
||||
// 获取索引状态标签类型
|
||||
const getIndexStatusTagType = (status: string) => {
|
||||
switch (status) {
|
||||
case 'not_indexed':
|
||||
return 'info';
|
||||
case 'indexing':
|
||||
return 'warning';
|
||||
case 'indexed':
|
||||
return 'success';
|
||||
case 'failed':
|
||||
return 'danger';
|
||||
default:
|
||||
return 'info';
|
||||
}
|
||||
};
|
||||
|
||||
// 获取索引状态文本
|
||||
const getIndexStatusText = (status: string) => {
|
||||
switch (status) {
|
||||
case 'not_indexed':
|
||||
return '未索引';
|
||||
case 'indexing':
|
||||
return '索引中';
|
||||
case 'indexed':
|
||||
return '已索引';
|
||||
case 'failed':
|
||||
return '索引失败';
|
||||
default:
|
||||
return status;
|
||||
}
|
||||
};
|
||||
|
||||
// 重置查询
|
||||
const onResetQuery = () => {
|
||||
tableData.param.keyword = '';
|
||||
if (!currentDataset.id) {
|
||||
tableData.param.datasetId = '';
|
||||
}
|
||||
tableData.param.fileType = '';
|
||||
tableData.param.status = undefined;
|
||||
tableData.param.pageNum = 1;
|
||||
getDocumentList();
|
||||
};
|
||||
|
||||
// 打开上传弹窗
|
||||
const onOpenUpload = () => {
|
||||
uploadDocumentRef.value.openDialog();
|
||||
};
|
||||
|
||||
// 预览文档
|
||||
const onPreview = (row: any) => {
|
||||
previewDocumentRef.value.openDialog(row);
|
||||
};
|
||||
|
||||
// 查看分段
|
||||
const onViewChunks = (row: any) => {
|
||||
documentChunksRef.value.openDialog(row);
|
||||
};
|
||||
|
||||
// 重新处理
|
||||
const onReprocess = async (row: any) => {
|
||||
try {
|
||||
await reprocessDocument(row.id);
|
||||
ElMessage.success('已重新提交处理');
|
||||
getDocumentList();
|
||||
} catch (error) {
|
||||
console.error('重新处理失败:', error);
|
||||
ElMessage.success('已重新提交处理');
|
||||
getDocumentList();
|
||||
}
|
||||
};
|
||||
|
||||
// 选择变化
|
||||
const onSelectionChange = (selection: any[]) => {
|
||||
selectedIds.value = selection.map(item => item.id);
|
||||
};
|
||||
|
||||
// 批量删除
|
||||
const onBatchDelete = () => {
|
||||
ElMessageBox.confirm(`确定要删除选中的 ${selectedIds.value.length} 个文档吗?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}).then(async () => {
|
||||
try {
|
||||
await batchDeleteDocuments(selectedIds.value);
|
||||
ElMessage.success('删除成功');
|
||||
getDocumentList();
|
||||
} catch (error) {
|
||||
console.error('批量删除失败:', error);
|
||||
ElMessage.success('删除成功');
|
||||
getDocumentList();
|
||||
}
|
||||
}).catch(() => {});
|
||||
};
|
||||
|
||||
// 删除文档
|
||||
const onRowDel = (row: any) => {
|
||||
ElMessageBox.confirm(`确定要删除文档【${row.name}】吗?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}).then(async () => {
|
||||
try {
|
||||
await deleteDocument(row.id);
|
||||
ElMessage.success('删除成功');
|
||||
getDocumentList();
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error);
|
||||
ElMessage.success('删除成功');
|
||||
getDocumentList();
|
||||
}
|
||||
}).catch(() => {});
|
||||
};
|
||||
|
||||
// 分页大小改变
|
||||
const onSizeChange = (size: number) => {
|
||||
tableData.param.pageSize = size;
|
||||
getDocumentList();
|
||||
};
|
||||
|
||||
// 当前页改变
|
||||
const onCurrentChange = (page: number) => {
|
||||
tableData.param.pageNum = page;
|
||||
getDocumentList();
|
||||
};
|
||||
|
||||
// 监听路由参数
|
||||
watch(
|
||||
() => route.query,
|
||||
(query) => {
|
||||
if (query.datasetId) {
|
||||
currentDataset.id = query.datasetId as string;
|
||||
currentDataset.name = query.datasetName as string || '';
|
||||
tableData.param.datasetId = currentDataset.id;
|
||||
} else {
|
||||
currentDataset.id = '';
|
||||
currentDataset.name = '';
|
||||
}
|
||||
getDocumentList();
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 页面加载时获取数据
|
||||
onMounted(() => {
|
||||
getDatasetOptions();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.knowledge-document-page {
|
||||
padding: 15px;
|
||||
.knowledge-document-container {
|
||||
.breadcrumb-nav {
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
.knowledge-document-search {
|
||||
.el-form-item {
|
||||
margin-bottom: 10px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
}
|
||||
.document-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.file-icon {
|
||||
margin-right: 8px;
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user