Files
admin-ui/src/views/knowledge/document/detail.vue

421 lines
10 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>
<div class="document-detail-page">
<!-- 面包屑导航 -->
<div class="page-header">
<el-breadcrumb separator=">">
<el-breadcrumb-item>
<span class="back-link" @click="onBackToknowledge">知识库</span>
</el-breadcrumb-item>
<el-breadcrumb-item>
<span class="back-link" @click="onBackToknowledge">{{ knowledgeName }}</span>
</el-breadcrumb-item>
<el-breadcrumb-item>{{ documentInfo.name }}</el-breadcrumb-item>
</el-breadcrumb>
</div>
<div class="page-content">
<!-- 左侧文档原文 -->
<div class="document-content">
<div class="content-header">
<h2>{{ documentInfo.name }}</h2>
<div class="content-meta">Size{{ formatFileSize(documentInfo.fileSize) }} Uploaded Time{{ documentInfo.createdAt }}</div>
</div>
<div class="content-body" v-loading="contentLoading">
<pre class="document-text">{{ documentContent }}</pre>
</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-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">
<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>
<div class="chunk-content">
<span class="chunk-text">{{ viewMode === 'full' ? chunk.content : truncateText(chunk.content, 100) }}</span>
</div>
<div class="chunk-actions">
<el-switch v-model="chunk.enabled" size="small" @change="onChunkStatusChange(chunk)" />
</div>
</div>
<el-empty v-if="filteredChunks.length === 0 && !chunkLoading" description="暂无切片" :image-size="60" />
</div>
<!-- 分页 -->
<div class="panel-footer">
<span class="total-info">总共 {{ chunkTotal }} </span>
<el-pagination
v-model:current-page="chunkPage"
:page-size="chunkPageSize"
:total="chunkTotal"
layout="prev, pager, next"
small
@current-change="getChunkList"
/>
<el-select v-model="chunkPageSize" size="small" style="width: 80px" @change="getChunkList">
<el-option :value="10" label="10条/页" />
<el-option :value="20" label="20条/页" />
<el-option :value="50" label="50条/页" />
</el-select>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
export default {
name: 'documentDetail',
};
</script>
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { ElMessage } from 'element-plus';
import { getDocument, listDocumentChunks, previewDocument } from '/@/api/knowledge/document';
const route = useRoute();
const router = useRouter();
// 路由参数
const knowledgeId = ref('');
const knowledgeName = ref('');
const documentId = ref('');
// 文档信息
const documentInfo = reactive({
id: '',
name: '',
fileType: '',
fileSize: 0,
createdAt: '',
});
// 文档内容
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 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 onBackToknowledge = () => {
router.push('/knowledge/knowledge');
};
// 返回数据集详情
const onBackToknowledge = () => {
router.push({
path: '/knowledge/document',
query: { knowledgeId: knowledgeId.value, knowledgeName: knowledgeName.value },
});
};
// 获取文档详情
const getDocumentDetail = async () => {
contentLoading.value = true;
try {
const [detailRes, contentRes] = await Promise.all([getDocument(documentId.value), previewDocument(documentId.value)]);
const detail = detailRes.data || {};
documentInfo.id = detail.id || documentId.value;
documentInfo.name = detail.name || (route.query.docName as string) || '';
documentInfo.fileType = detail.fileType || '';
documentInfo.fileSize = detail.fileSize || 0;
documentInfo.createdAt = detail.createdAt || '';
const contentData = contentRes.data;
documentContent.value = typeof contentData === 'string' ? contentData : contentData?.content || contentData?.text || '';
} catch (_error) {
ElMessage.error('获取文档详情失败');
documentContent.value = '';
} finally {
contentLoading.value = false;
}
};
// 获取切片列表
const getChunkList = async () => {
chunkLoading.value = true;
try {
const res: any = await listDocumentChunks({
documentId: documentId.value,
pageNum: chunkPage.value,
pageSize: chunkPageSize.value,
});
chunkList.value = (res.data?.list || []).map((item: any) => ({
...item,
enabled: true,
selected: false,
}));
chunkTotal.value = res.data?.total || 0;
} catch (_error) {
chunkList.value = [];
chunkTotal.value = 0;
ElMessage.error('获取切片列表失败');
} finally {
chunkLoading.value = false;
}
};
// 全选变化
const onSelectAllChange = (val: boolean) => {
chunkList.value.forEach((chunk) => {
chunk.selected = val;
});
};
// 切片状态变化
const onChunkStatusChange = (_chunk: any) => {
ElMessage.info('切片启停功能暂未开放');
};
// 添加切片
const onAddChunk = () => {
ElMessage.info('添加切片功能开发中');
};
// 初始化
onMounted(() => {
knowledgeId.value = (route.query.knowledgeId as string) || '';
knowledgeName.value = (route.query.knowledgeName as string) || '';
documentId.value = (route.query.docId as string) || '';
if (!documentId.value) {
ElMessage.warning('缺少文档ID无法查看详情');
onBackToknowledge();
return;
}
getDocumentDetail();
getChunkList();
});
</script>
<style scoped lang="scss">
.document-detail-page {
height: 100%;
display: flex;
flex-direction: column;
background: #f5f7fa;
.page-header {
padding: 12px 20px;
background: #fff;
border-bottom: 1px solid #ebeef5;
.back-link {
color: #409eff;
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
}
.page-content {
flex: 1;
display: flex;
overflow: hidden;
padding: 15px;
gap: 15px;
// 左侧文档内容
.document-content {
flex: 1;
display: flex;
flex-direction: column;
background: #fff;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
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;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
.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;
}
}
.chunk-list {
flex: 1;
overflow: auto;
padding: 12px 20px;
.select-all {
margin-bottom: 12px;
}
.chunk-item {
display: flex;
align-items: flex-start;
padding: 12px;
background: #f5f7fa;
border-radius: 6px;
margin-bottom: 8px;
border: 1px solid #ebeef5;
.chunk-checkbox {
margin-right: 12px;
padding-top: 2px;
}
.chunk-content {
flex: 1;
min-width: 0;
.chunk-text {
font-size: 14px;
color: #303133;
line-height: 1.5;
word-break: break-all;
}
}
.chunk-actions {
margin-left: 12px;
flex-shrink: 0;
}
}
}
.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>