Files
admin-ui/src/views/knowledge/component/documentDetailDialog.vue
2910410219 4f547b5bff feat(知识库): 添加模型配置管理功能并修复向量状态显示
添加模型配置管理相关功能,包括模型配置列表展示、创建和编辑功能。同时修复文档详情中向量状态显示问题,将数字类型转换为布尔类型以正确绑定到el-switch组件。

- 新增模型配置相关API接口和类型定义
- 添加模型配置列表弹窗及创建/编辑表单
- 修复向量状态显示问题,确保与el-switch组件正确绑定
- 优化深拷贝逻辑,自动转换status字段类型
2026-04-16 14:49:33 +08:00

466 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<el-drawer
v-model="visible"
:title="documentInfo.name || '文档详情'"
size="80%"
direction="rtl"
:close-on-click-modal="true"
destroy-on-close
class="document-detail-drawer"
>
<div class="drawer-content">
<!-- 左侧文档原文 -->
<div class="document-content">
<div class="content-header" style="display: flex; justify-content: space-between; align-items: flex-start">
<div>
<h2>{{ documentInfo.name }}</h2>
<div class="content-meta">Size{{ formatFileSize(documentInfo.fileSize) }} Uploaded Time{{ documentInfo.createdAt }}</div>
</div>
<a
href="#"
id="downloadLink"
style="
background-color: #f0f9eb;
color: #67c23a;
padding: 6px 12px;
border-radius: 4px;
text-decoration: none;
font-size: 12px;
display: inline-flex;
align-items: center;
border: 1px solid #e1f5dc;
margin-top: 4px;
"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
style="margin-right: 4px"
>
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="7 10 12 15 17 10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</svg>
下载
</a>
</div>
<div class="content-body" v-loading="contentLoading">
<div class="document-text" v-html="documentContent"></div>
</div>
</div>
<!-- 右侧向量列表 -->
<div class="chunk-panel">
<div class="panel-header">
<h3>向量列表</h3>
<div class="panel-subtitle">查看文档的向量信息</div>
</div>
<div class="panel-toolbar">
<div class="toolbar-actions">
<el-input v-model="vectorSearch" placeholder="搜索" clearable size="small" style="width: 120px">
<template #prefix>
<el-icon><ele-Search /></el-icon>
</template>
</el-input>
</div>
</div>
<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 ? '启用' : '禁用' }}</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="vector-content">
<span class="vector-text">{{ truncateText(vector.content, 150) }}</span>
</div>
<div class="vector-meta">
<span class="vector-hash">哈希: {{ vector.contentHash }}</span>
<span class="vector-time">创建时间: {{ vector.createdAt }}</span>
</div>
</div>
<el-empty v-if="vectorList.length === 0 && !vectorLoading" description="暂无向量" :image-size="60" />
</div>
<!-- 分页 -->
<div class="panel-footer">
<span class="total-info">总共 {{ vectorTotal }} </span>
<el-pagination
v-model:current-page="vectorPage"
:page-size="vectorPageSize"
:total="vectorTotal"
layout="prev, pager, next"
small
@current-change="getVectorList"
/>
<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条/页" />
</el-select>
</div>
</div>
</div>
</el-drawer>
</template>
<script lang="ts">
export default {
name: 'documentDetailDialog',
};
</script>
<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;
knowledgeId: string;
knowledgeName: string;
document: any;
}>();
const emit = defineEmits(['update:modelValue']);
const visible = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val),
});
// 文档信息
const documentInfo = reactive({
id: '',
name: '',
fileType: '',
fileSize: 0,
createdAt: '',
});
// 文档内容
const documentContent = ref('');
const contentLoading = ref(false);
// 向量列表相关
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) => {
if (!size) return '0 Bytes';
if (size < 1024) return size + ' Bytes';
if (size < 1024 * 1024) return (size / 1024).toFixed(0) + ' KB';
return (size / 1024 / 1024).toFixed(1) + ' MB';
};
// 截断文本
const truncateText = (text: string, maxLength: number) => {
if (!text || text.length <= maxLength) return text;
return text.substring(0, maxLength) + '...';
};
// 获取文档详情
const getDocumentDetail = async () => {
contentLoading.value = true;
try {
// 使用实际的文档数据
documentInfo.id = props.document?.id || '';
documentInfo.name = props.document?.title || '未知文件';
documentInfo.fileSize = props.document?.fileSize || 0;
documentInfo.createdAt = props.document?.createdAt || '';
// 构建文件地址
const imgAddressPrefix = props.document?.imgAddressPrefix?.trim() || '';
const filePath = props.document?.filePath || '';
const fileUrl = imgAddressPrefix + filePath;
// 设置下载链接
nextTick(() => {
const downloadLink = document.getElementById('downloadLink');
if (downloadLink) {
downloadLink.setAttribute('href', fileUrl);
downloadLink.setAttribute('download', documentInfo.name);
}
});
// 尝试获取文件内容
let fileContent = '';
try {
const response = await fetch(fileUrl);
if (response.ok) {
fileContent = await response.text();
} else {
fileContent = '无法加载文件内容';
}
} catch (error) {
fileContent = '无法加载文件内容';
}
// 生成文档内容,只包含文件内容
documentContent.value = `<pre style="background: #fafafa; padding: 16px; border-radius: 4px; border: 1px solid #ebeef5; margin: 0; white-space: pre-wrap; word-break: break-all; font-size: 14px; line-height: 1.5;">${fileContent}</pre>`;
} catch (_error) {
ElMessage.error('获取文档详情失败');
documentContent.value = '';
} finally {
contentLoading.value = false;
}
};
// 获取向量列表
const getVectorList = async () => {
vectorLoading.value = true;
try {
const response = await listDocumentVectors({
documentId: props.document?.id,
datasetId: props.document?.datasetId,
pageNum: vectorPage.value,
pageSize: vectorPageSize.value,
});
// 深拷贝数据避免v-model触发不必要的更新并将status从数字转换为布尔类型
vectorList.value = (response.data?.list || []).map((item: any) => {
const clonedItem = JSON.parse(JSON.stringify(item));
// 将数字类型的status转换为布尔类型以便正确绑定到el-switch
clonedItem.status = clonedItem.status === 1;
return clonedItem;
});
vectorTotal.value = response.data?.total || 0;
} catch (_error) {
ElMessage.error('获取向量列表失败');
vectorList.value = [];
vectorTotal.value = 0;
} finally {
vectorLoading.value = false;
// 初始化完成,允许状态更新
setTimeout(() => {
isInitializing.value = false;
}, 100);
}
};
// 更新向量状态
const onVectorStatusChange = async (vector: any, oldValue: boolean) => {
// 初始化过程中不处理状态变化
if (isInitializing.value) {
return;
}
// 计算新状态ElSwitch的v-model是boolean需要转换为数字
const newStatus = vector.status ? 1 : 0;
const oldStatus = oldValue ? 1 : 0;
// 只在状态真正改变时才调用API
if (newStatus !== oldStatus) {
try {
await updateDocumentVector({
id: String(vector.id), // 确保id是字符串类型
status: newStatus,
});
ElMessage.success('更新成功');
} catch (error) {
ElMessage.error('更新失败');
// 恢复原始状态
vector.status = oldValue;
}
}
};
// 监听弹窗打开
watch(
() => props.modelValue,
(val) => {
if (val && props.document) {
// 重置初始化标志
isInitializing.value = true;
getDocumentDetail();
getVectorList();
}
}
);
</script>
<style scoped lang="scss">
.document-detail-drawer {
:deep(.el-drawer__body) {
padding: 0;
overflow: hidden;
}
}
.drawer-content {
display: flex;
height: 100%;
// 左侧文档内容
.document-content {
flex: 1;
display: flex;
flex-direction: column;
border-right: 1px solid #ebeef5;
overflow: hidden;
.content-header {
padding: 16px 20px;
border-bottom: 1px solid #ebeef5;
h2 {
margin: 0 0 8px 0;
font-size: 18px;
font-weight: 600;
color: #303133;
}
.content-meta {
font-size: 12px;
color: #909399;
}
}
.content-body {
flex: 1;
padding: 16px 20px;
overflow: auto;
background: #fafafa;
.document-text {
margin: 0;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 14px;
line-height: 1.6;
color: #303133;
white-space: pre-wrap;
word-break: break-all;
}
}
}
// 右侧切片面板
.chunk-panel {
width: 420px;
display: flex;
flex-direction: column;
background: #fff;
.panel-header {
padding: 16px 20px;
border-bottom: 1px solid #ebeef5;
h3 {
margin: 0 0 4px 0;
font-size: 16px;
font-weight: 600;
color: #303133;
}
.panel-subtitle {
font-size: 12px;
color: #909399;
}
}
.panel-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 20px;
border-bottom: 1px solid #ebeef5;
.toolbar-actions {
display: flex;
gap: 8px;
}
}
.vector-list {
flex: 1;
overflow: auto;
padding: 12px 20px;
.vector-item {
display: flex;
flex-direction: column;
padding: 12px;
background: #f5f7fa;
border-radius: 6px;
margin-bottom: 8px;
border: 1px solid #ebeef5;
.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;
}
}
.vector-content {
margin-bottom: 8px;
.vector-text {
font-size: 14px;
color: #303133;
line-height: 1.5;
word-break: break-all;
}
}
.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;
}
}
}
}
.panel-footer {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 12px;
padding: 12px 20px;
border-top: 1px solid #ebeef5;
.total-info {
font-size: 12px;
color: #909399;
}
}
}
}
</style>