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

426 lines
9.5 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="onBackToDataset">{{ datasetName }}</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, index) 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';
const route = useRoute();
const router = useRouter();
// 路由参数
const datasetId = ref('');
const datasetName = 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');
};
// 返回数据集详情
const onBackToDataset = () => {
router.push({
path: '/knowledge',
query: { datasetId: datasetId.value, datasetName: datasetName.value }
});
};
// 获取文档详情
const getDocumentDetail = async () => {
contentLoading.value = true;
try {
// 模拟数据
documentInfo.id = documentId.value;
documentInfo.name = route.query.docName as string || '456_product(1).txt';
documentInfo.fileType = 'txt';
documentInfo.fileSize = 10;
documentInfo.createdAt = '22/01/2026 00:53:32';
// 模拟文档内容
documentContent.value = '<p>123</p>';
} catch (error) {
console.error('获取文档详情失败:', error);
} finally {
contentLoading.value = false;
}
};
// 获取切片列表
const getChunkList = async () => {
chunkLoading.value = true;
try {
// 模拟数据
chunkList.value = [
{
id: '1',
content: '123',
enabled: true,
selected: false,
},
];
chunkTotal.value = 1;
} catch (error) {
console.error('获取切片列表失败:', error);
} finally {
chunkLoading.value = false;
}
};
// 全选变化
const onSelectAllChange = (val: boolean) => {
chunkList.value.forEach(chunk => {
chunk.selected = val;
});
};
// 切片状态变化
const onChunkStatusChange = (chunk: any) => {
ElMessage.success(chunk.enabled ? '已启用' : '已禁用');
};
// 添加切片
const onAddChunk = () => {
ElMessage.info('添加切片功能开发中');
};
// 初始化
onMounted(() => {
datasetId.value = route.query.datasetId as string || '';
datasetName.value = route.query.datasetName as string || 'dataset_tenant_1';
documentId.value = route.query.docId as string || '';
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>