feat(文档向量): 添加文档向量管理功能
- 新增文档向量查询接口和更新接口 - 重构文档详情弹窗,将切片展示改为向量列表展示 - 优化表格模板语法使用解构写法 - 统一文件计数字段名为documentCount
This commit is contained in:
@@ -54,6 +54,30 @@ export interface DocumentInfo {
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
// 文件块向量查询参数
|
||||
export interface DocumentVectorQueryParams {
|
||||
documentId?: string;
|
||||
datasetId?: string;
|
||||
pageNum: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
// 文件块向量信息
|
||||
export interface DocumentVectorInfo {
|
||||
id: number;
|
||||
status: number;
|
||||
vectorStatus: number;
|
||||
datasetId: number;
|
||||
documentId: number;
|
||||
content: string;
|
||||
contentHash: string;
|
||||
chunkIndex: number;
|
||||
vector: number[];
|
||||
metadata: Record<string, any>;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
// 获取文档列表
|
||||
export function listDocuments(params: DocumentQueryParams) {
|
||||
return request({
|
||||
@@ -194,3 +218,21 @@ export function generateVector(id: string, datasetId: string) {
|
||||
data: { id, datasetId },
|
||||
});
|
||||
}
|
||||
|
||||
// 获取文件块向量列表
|
||||
export function listDocumentVectors(params: DocumentVectorQueryParams) {
|
||||
return request({
|
||||
url: '/rag/document/vector/list',
|
||||
method: 'get',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
// 更新文件块
|
||||
export function updateDocumentVector(data: any) {
|
||||
return request({
|
||||
url: '/rag/document/vector/update',
|
||||
method: 'put',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -56,64 +56,55 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:切片结果 -->
|
||||
<!-- 右侧:向量列表 -->
|
||||
<div class="chunk-panel">
|
||||
<div class="panel-header">
|
||||
<h3>切片结果</h3>
|
||||
<div class="panel-subtitle">查看用于嵌入和召回的切片段落。</div>
|
||||
<h3>向量列表</h3>
|
||||
<div class="panel-subtitle">查看文档的向量信息。</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-toolbar">
|
||||
<div class="toolbar-tabs">
|
||||
<el-radio-group v-model="viewMode" size="small">
|
||||
<el-radio-button label="full">全文</el-radio-button>
|
||||
<el-radio-button label="chunk">省略</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<div class="toolbar-actions">
|
||||
<el-input v-model="chunkSearch" placeholder="搜索" clearable size="small" style="width: 120px">
|
||||
<el-input v-model="vectorSearch" placeholder="搜索" clearable size="small" style="width: 120px">
|
||||
<template #prefix>
|
||||
<el-icon><ele-Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button size="small" @click="onAddChunk">
|
||||
<el-icon><ele-Plus /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chunk-list" v-loading="chunkLoading">
|
||||
<div class="select-all">
|
||||
<el-checkbox v-model="selectAll" @change="onSelectAllChange">选择所有</el-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="chunk-item" v-for="chunk in filteredChunks" :key="chunk.id">
|
||||
<div class="chunk-checkbox">
|
||||
<el-checkbox v-model="chunk.selected" />
|
||||
<div class="vector-list" v-loading="vectorLoading">
|
||||
<div class="vector-item" v-for="vector in vectorList" :key="vector.id">
|
||||
<div class="vector-header">
|
||||
<span class="vector-index">块 {{ vector.chunkIndex }}</span>
|
||||
<span class="vector-status">状态: {{ vector.status === 1 ? '启用' : '禁用' }}</span>
|
||||
<span class="vector-vector-status">向量状态: {{ vector.vectorStatus === 1 ? '已生成' : '未生成' }}</span>
|
||||
<el-switch v-model="vector.status" size="small" @change="(value: boolean) => onVectorStatusChange(vector, !value)" />
|
||||
</div>
|
||||
<div class="chunk-content">
|
||||
<span class="chunk-text">{{ viewMode === 'full' ? chunk.content : truncateText(chunk.content, 100) }}</span>
|
||||
<div class="vector-content">
|
||||
<span class="vector-text">{{ truncateText(vector.content, 150) }}</span>
|
||||
</div>
|
||||
<div class="chunk-actions">
|
||||
<el-switch v-model="chunk.enabled" size="small" @change="onChunkStatusChange(chunk)" />
|
||||
<div class="vector-meta">
|
||||
<span class="vector-hash">哈希: {{ vector.contentHash }}</span>
|
||||
<span class="vector-time">创建时间: {{ vector.createdAt }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-empty v-if="filteredChunks.length === 0 && !chunkLoading" description="暂无切片" :image-size="60" />
|
||||
<el-empty v-if="vectorList.length === 0 && !vectorLoading" description="暂无向量" :image-size="60" />
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="panel-footer">
|
||||
<span class="total-info">总共 {{ chunkTotal }} 条</span>
|
||||
<span class="total-info">总共 {{ vectorTotal }} 条</span>
|
||||
<el-pagination
|
||||
v-model:current-page="chunkPage"
|
||||
:page-size="chunkPageSize"
|
||||
:total="chunkTotal"
|
||||
v-model:current-page="vectorPage"
|
||||
:page-size="vectorPageSize"
|
||||
:total="vectorTotal"
|
||||
layout="prev, pager, next"
|
||||
small
|
||||
@current-change="getChunkList"
|
||||
@current-change="getVectorList"
|
||||
/>
|
||||
<el-select v-model="chunkPageSize" size="small" style="width: 80px" @change="getChunkList">
|
||||
<el-select v-model="vectorPageSize" size="small" style="width: 80px" @change="getVectorList">
|
||||
<el-option :value="10" label="10条/页" />
|
||||
<el-option :value="20" label="20条/页" />
|
||||
<el-option :value="50" label="50条/页" />
|
||||
@@ -133,6 +124,7 @@ export default {
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, watch, nextTick } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { listDocumentVectors, updateDocumentVector } from '/@/api/knowledge/document';
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean;
|
||||
@@ -161,21 +153,14 @@ const documentInfo = reactive({
|
||||
const documentContent = ref('');
|
||||
const contentLoading = ref(false);
|
||||
|
||||
// 切片相关
|
||||
const chunkLoading = ref(false);
|
||||
const chunkList = ref<any[]>([]);
|
||||
const chunkTotal = ref(0);
|
||||
const chunkPage = ref(1);
|
||||
const chunkPageSize = ref(50);
|
||||
const chunkSearch = ref('');
|
||||
const viewMode = ref('full');
|
||||
const selectAll = ref(false);
|
||||
|
||||
// 过滤后的切片列表
|
||||
const filteredChunks = computed(() => {
|
||||
if (!chunkSearch.value) return chunkList.value;
|
||||
return chunkList.value.filter((chunk) => (chunk.content || '').toLowerCase().includes(chunkSearch.value.toLowerCase()));
|
||||
});
|
||||
// 向量列表相关
|
||||
const vectorLoading = ref(false);
|
||||
const vectorList = ref<any[]>([]);
|
||||
const vectorTotal = ref(0);
|
||||
const vectorPage = ref(1);
|
||||
const vectorPageSize = ref(50);
|
||||
const vectorSearch = ref('');
|
||||
const isInitializing = ref(true); // 初始化标志,用于防止加载时触发更新
|
||||
|
||||
// 格式化文件大小
|
||||
const formatFileSize = (size: number) => {
|
||||
@@ -238,44 +223,57 @@ const getDocumentDetail = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 获取切片列表
|
||||
const getChunkList = async () => {
|
||||
chunkLoading.value = true;
|
||||
// 获取向量列表
|
||||
const getVectorList = async () => {
|
||||
vectorLoading.value = true;
|
||||
try {
|
||||
// 模拟数据
|
||||
chunkList.value = [
|
||||
{
|
||||
id: '1',
|
||||
content: '123',
|
||||
enabled: true,
|
||||
selected: false,
|
||||
},
|
||||
];
|
||||
chunkTotal.value = 1;
|
||||
const response = await listDocumentVectors({
|
||||
documentId: props.document?.id,
|
||||
datasetId: props.document?.datasetId,
|
||||
pageNum: vectorPage.value,
|
||||
pageSize: vectorPageSize.value,
|
||||
});
|
||||
// 深拷贝数据,避免v-model触发不必要的更新
|
||||
vectorList.value = (response.data?.list || []).map((item: any) => JSON.parse(JSON.stringify(item)));
|
||||
vectorTotal.value = response.data?.total || 0;
|
||||
} catch (_error) {
|
||||
ElMessage.error('获取切片列表失败');
|
||||
chunkList.value = [];
|
||||
chunkTotal.value = 0;
|
||||
ElMessage.error('获取向量列表失败');
|
||||
vectorList.value = [];
|
||||
vectorTotal.value = 0;
|
||||
} finally {
|
||||
chunkLoading.value = false;
|
||||
vectorLoading.value = false;
|
||||
// 初始化完成,允许状态更新
|
||||
setTimeout(() => {
|
||||
isInitializing.value = false;
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
|
||||
// 全选变化
|
||||
const onSelectAllChange = (val: boolean) => {
|
||||
chunkList.value.forEach((chunk) => {
|
||||
chunk.selected = val;
|
||||
});
|
||||
};
|
||||
// 更新向量状态
|
||||
const onVectorStatusChange = async (vector: any, oldValue: boolean) => {
|
||||
// 初始化过程中不处理状态变化
|
||||
if (isInitializing.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 切片状态变化
|
||||
const onChunkStatusChange = (chunk: any) => {
|
||||
ElMessage.success(chunk.enabled ? '已启用' : '已禁用');
|
||||
};
|
||||
// 计算新状态(ElSwitch的v-model是boolean,需要转换为数字)
|
||||
const newStatus = vector.status ? 1 : 0;
|
||||
const oldStatus = oldValue ? 1 : 0;
|
||||
|
||||
// 添加切片
|
||||
const onAddChunk = () => {
|
||||
ElMessage.info('添加切片功能开发中');
|
||||
// 只在状态真正改变时才调用API
|
||||
if (newStatus !== oldStatus) {
|
||||
try {
|
||||
await updateDocumentVector({
|
||||
id: vector.id,
|
||||
status: newStatus,
|
||||
});
|
||||
ElMessage.success('更新成功');
|
||||
} catch (error) {
|
||||
ElMessage.error('更新失败');
|
||||
// 恢复原始状态
|
||||
vector.status = oldValue;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 监听弹窗打开
|
||||
@@ -283,8 +281,10 @@ watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
if (val && props.document) {
|
||||
// 重置初始化标志
|
||||
isInitializing.value = true;
|
||||
getDocumentDetail();
|
||||
getChunkList();
|
||||
getVectorList();
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -382,34 +382,43 @@ watch(
|
||||
}
|
||||
}
|
||||
|
||||
.chunk-list {
|
||||
.vector-list {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 12px 20px;
|
||||
|
||||
.select-all {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.chunk-item {
|
||||
.vector-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
padding: 12px;
|
||||
background: #f5f7fa;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 8px;
|
||||
border: 1px solid #ebeef5;
|
||||
|
||||
.chunk-checkbox {
|
||||
margin-right: 12px;
|
||||
padding-top: 2px;
|
||||
.vector-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
|
||||
.vector-index {
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.el-switch {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.chunk-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
.vector-content {
|
||||
margin-bottom: 8px;
|
||||
|
||||
.chunk-text {
|
||||
.vector-text {
|
||||
font-size: 14px;
|
||||
color: #303133;
|
||||
line-height: 1.5;
|
||||
@@ -417,9 +426,18 @@ watch(
|
||||
}
|
||||
}
|
||||
|
||||
.chunk-actions {
|
||||
margin-left: 12px;
|
||||
flex-shrink: 0;
|
||||
.vector-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
|
||||
.vector-hash {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 200px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
</div>
|
||||
<div class="card-info">
|
||||
<div class="card-name">{{ item.name }}</div>
|
||||
<div class="card-meta">{{ item.fileCount || 0 }} 个文件</div>
|
||||
<div class="card-meta">{{ item.documentCount || 0 }} 个文件</div>
|
||||
<div class="card-time">{{ item.createdAt }}</div>
|
||||
</div>
|
||||
<!-- 悬停操作按钮 -->
|
||||
@@ -78,7 +78,7 @@
|
||||
</div>
|
||||
<div class="profile-info">
|
||||
<div class="profile-name">{{ currentknowledge.name }}</div>
|
||||
<div class="profile-meta">{{ currentknowledge.fileCount || 0 }} 个文件 · {{ formatFileSize(currentknowledge.totalSize || 0) }}</div>
|
||||
<div class="profile-meta">{{ currentknowledge.documentCount || 0 }} 个文件 · {{ formatFileSize(currentknowledge.totalSize || 0) }}</div>
|
||||
<div class="profile-time">创建于 {{ currentknowledge.createdAt }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -128,37 +128,29 @@
|
||||
<div class="file-table" v-loading="fileLoading">
|
||||
<el-table :data="fileList" style="width: 100%" row-key="id" border>
|
||||
<el-table-column prop="title" label="名称" min-width="200">
|
||||
<template #default="scope">
|
||||
<span class="file-link" @click="onViewDocumentDetail(scope.row)" style="cursor: pointer; color: #409eff">{{
|
||||
scope.row.title
|
||||
}}</span>
|
||||
<template #default="{ row }">
|
||||
<span class="file-link" @click="onViewDocumentDetail(row)" style="cursor: pointer; color: #409eff">{{ row.title }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="chunkCount" label="分块数" width="80" align="center" />
|
||||
<el-table-column prop="status" label="状态" width="90" align="center">
|
||||
<template #default="scope">
|
||||
<el-switch
|
||||
v-model="scope.row.statusEnabled"
|
||||
inline-prompt
|
||||
active-text="启"
|
||||
inactive-text="停"
|
||||
@change="onFileStatusChange(scope.row)"
|
||||
/>
|
||||
<template #default="{ row }">
|
||||
<el-switch v-model="row.statusEnabled" inline-prompt active-text="启" inactive-text="停" @change="onFileStatusChange(row)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="vectorStatus" label="向量化" width="100" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.vectorStatus === 2 ? 'success' : 'warning'" size="small">
|
||||
{{ scope.row.vectorStatus === 2 ? '已完成' : '未完成' }}
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.vectorStatus === 2 ? 'success' : 'warning'" size="small">
|
||||
{{ row.vectorStatus === 2 ? '已完成' : '未完成' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createdAt" label="上传日期" width="180" />
|
||||
<el-table-column label="动作" width="180" align="center">
|
||||
<template #default="scope">
|
||||
<el-button text size="small" @click="onPreviewFile(scope.row)">预览</el-button>
|
||||
<el-button text size="small" type="primary" @click="onGenerateVector(scope.row)">生成向量</el-button>
|
||||
<el-button text size="small" type="danger" @click="onDeleteFile(scope.row)">删除</el-button>
|
||||
<template #default="{ row }">
|
||||
<el-button text size="small" @click="onPreviewFile(row)">预览</el-button>
|
||||
<el-button text size="small" type="primary" @click="onGenerateVector(row)">生成向量</el-button>
|
||||
<el-button text size="small" type="danger" @click="onDeleteFile(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
Reference in New Issue
Block a user