完善分段预览功能,添加懒加载,添加分段预览详情

This commit is contained in:
moon 2025-09-22 15:27:38 +08:00
parent 919ab51570
commit 653770e983
7 changed files with 201 additions and 206 deletions

View File

@ -1,6 +1,6 @@
import request from '@/utils/request'
export function getSegmentList(params: { datasetId: string; documentId: string }) {
export function getSegmentList(params: { datasetId: string; documentId: string; page: number; pageSize: number }) {
return request({
url: '/brichat-service/documentSegment/selectSegments',
method: 'get',

View File

@ -88,7 +88,6 @@
:header-row-style="{ height: '50px' }"
@selection-change="handleSelectionChange"
@row-dblclick="handleRowDoubleClick"
@sort-change="handleSortChange"
>
<el-table-column type="selection" width="55" />
<el-table-column
@ -98,7 +97,7 @@
type="index"
:index="indexMethod"
/>
<el-table-column prop="fileName" :label="t('vabI18n.knowledge.document.table.fileName')" min-width="250" sortable="custom" :sort-orders="['ascending', 'descending']">
<el-table-column prop="fileName" :label="t('vabI18n.knowledge.document.table.fileName')" min-width="250">
<template #default="{ row }">
<div class="file-name-cell">
<img
@ -133,14 +132,14 @@
<span v-else class="folder-status">--</span>
</template>
</el-table-column>
<el-table-column prop="createDate" :label="t('vabI18n.knowledge.document.table.createDate')" width="180" sortable="custom" :sort-orders="['ascending', 'descending']" />
<el-table-column prop="charCount" :label="t('vabI18n.knowledge.document.table.charCount')" width="120" align="right" sortable="custom" :sort-orders="['ascending', 'descending']">
<el-table-column prop="createDate" :label="t('vabI18n.knowledge.document.table.createDate')" width="180" />
<el-table-column prop="charCount" :label="t('vabI18n.knowledge.document.table.charCount')" width="120" align="right">
<template #default="{ row }">
<span class="char-count" v-if="row.type === 'file'">{{ formatFileSize(row.charCount) }}</span>
<span v-else class="folder-indicator">--</span>
</template>
</el-table-column>
<el-table-column :label="t('vabI18n.knowledge.document.table.actions')" width="380" fixed="right">
<el-table-column :label="t('vabI18n.knowledge.document.table.actions')" width="320" fixed="right">
<template #default="{ row }">
<div class="action-buttons">
<!-- 文件操作 -->
@ -167,15 +166,6 @@
>
{{t('vabI18n.knowledge.document.buttons.download')}}
</el-button>
<el-button
type="warning"
:icon="Upload"
text
class="action-btn"
@click="handleFileReplaceFromRow(row)"
>
替换
</el-button>
</template>
<!-- 文件夹操作 -->
<template v-else>
@ -245,43 +235,6 @@
/>
</el-dialog>
<!-- 文件替换对话框 -->
<el-dialog
v-model="replaceDialogVisible"
title="替换文件"
width="900px"
:close-on-click-modal="false"
class="upload-dialog"
>
<div v-if="replaceTargetFile" class="replace-info">
<div class="target-file-info">
<h4>将要替换的文件</h4>
<div class="file-info-card">
<img
:src="getFileTypeIcon(replaceTargetFile.fileType)"
alt="文件图标"
class="file-icon"
/>
<div class="file-details">
<div class="file-name">{{ replaceTargetFile.fileName }}</div>
<div class="file-meta">
<span>大小{{ formatFileSize(replaceTargetFile.charCount) }}</span>
<span>创建时间{{ replaceTargetFile.createDate }}</span>
</div>
</div>
</div>
</div>
</div>
<DocUpload
:visible="replaceDialogVisible"
:dataset-id="datasetId"
:parent-id="currentParentId"
:replace-file-id="replaceTargetFile?.id"
@update:visible="replaceDialogVisible = $event"
@success="handleReplaceSuccess"
/>
</el-dialog>
<!-- 预览抽屉 -->
<el-drawer
@ -296,17 +249,23 @@
<div v-loading="previewLoading" class="drawer-content-container" style="display: flex; height: 100%;">
<!-- 左侧分段数据容器 -->
<div class="segment-list-container" style="width: 30%; border-right: 1px solid #ebeef5; overflow-y: auto; padding: 10px;">
<div class="segment-list-container"
style="width: 50%; border-right: 1px solid #ebeef5; overflow-y: auto; padding: 10px;"
@scroll="handleScroll"
>
<h3>分段数据</h3>
<el-form v-if="segmentList.length > 0">
<el-form-item
v-for="(segment) in segmentList"
:key="segment.id"
style="cursor: pointer; margin-bottom: 5px;"
@click="handlePreviewSegment(segment)"
>
<span>分段{{segment.position}}</span>
<h4>{{segment.content}}</h4>
</el-form-item>
<el-form-item v-if="isScroll" v-loading="segmentIsLoading" style="text-align: center; margin-top: 10px;">
</el-form-item>
</el-form>
<div v-else>
暂无分段数据
@ -351,6 +310,23 @@
</div>
</el-drawer>
<el-dialog
v-model="previewDialogVisible"
:title="分段详情"
:direction="'rtl'"
size="80%"
class="preview-drawer"
:close-on-click-modal="false"
@close="handleClosePreviewDialog"
>
<div class="sgement-info">
<div>
<span>分段{{previewSegment.position}}</span>
<h4>{{previewSegment.content}}</h4>
</div>
</div>
</el-dialog>
<!-- 重命名对话框 -->
<el-dialog
@ -533,7 +509,12 @@ const { t, locale } = useI18n()
const userStore = useUserStore()
const aclStore = useAclStore()
const { token } = userStore
const segmentIsLoading = ref(false)
const segmentPage = ref(2)
const segmentPageSize = ref(10)
const segmentdocId = ref('')
const previewSegment = ref("")
const previewDialogVisible = ref(false)
const route = useRoute();
const router = useRouter();
const datasetId = ref('');
@ -541,8 +522,6 @@ const datasetName = ref('');
const previewLoading = ref(true)
const uploadDialogVisible = ref(false)
const replaceDialogVisible = ref(false)
const replaceTargetFile = ref<FileItem | null>(null)
//
const renameDialogVisible = ref(false)
@ -578,6 +557,7 @@ y: 0
//
const currentParentId = ref<number | undefined>(undefined)
const breadcrumbPath = ref<Array<{id: number, name: string}>>([])
const isScroll = ref(true)
//
interface BreadcrumbItem {
@ -597,8 +577,6 @@ const handleClose = () => {
onUnmounted(() => {
//
document.removeEventListener('click', hideContextMenu)
//
stopAutoRefresh()
})
const getFileTypeIcon = (fileType: string) => {
@ -680,12 +658,6 @@ size: 20,
total: 0
})
//
const sortConfig = reactive({
orderBy: 'name',
orderDirection: 'ASC'
})
//
const fileList = ref<FileItem[]>([])
@ -857,12 +829,6 @@ const triggerFileInput = () => {
uploadDialogVisible.value = true
}
// -
const handleFileReplaceFromRow = (row: FileItem) => {
replaceTargetFile.value = row
replaceDialogVisible.value = true
}
const indexMethod = (index: number) => {
return (pagination.current - 1) * pagination.size + index + 1
}
@ -873,22 +839,9 @@ return (pagination.current - 1) * pagination.size + index + 1
const handleUploadSuccess = async () => {
uploadDialogVisible.value = false
await fetchDocuments()
// ElNotification({
// title: t('vabI18n.knowledge.document.messages.uploadSuccess'),
// message: t('vabI18n.knowledge.document.messages.uploadSuccessEnd'),
// type: 'success'
// })
}
//
const handleReplaceSuccess = async () => {
replaceDialogVisible.value = false
replaceTargetFile.value = null
selectedRows.value = [] //
await fetchDocuments()
ElNotification({
title: '替换成功',
message: '文件替换成功',
title: t('vabI18n.knowledge.document.messages.uploadSuccess'),
message: t('vabI18n.knowledge.document.messages.uploadSuccessEnd'),
type: 'success'
})
}
@ -990,12 +943,17 @@ const handlePreview = async (row: FileItem) => {
console.log("previewFileUrl (from API)", previewFileUrl.value)
}
}
segmentdocId.value = row.difyDocId
const resp = await getSegmentList({
datasetId:datasetId.value,
documentId: row.difyDocId
documentId: row.difyDocId,
page: 1,
pageSize: 10
})
if (resp.data?.length) {
segmentList.value = resp.data
console.log("resp.data",resp)
console.log(segmentdocId)
if (resp.data.list?.length) {
segmentList.value = resp.data.list
}
} catch (error) {
ElNotification({
@ -1013,28 +971,6 @@ const handlePreview = async (row: FileItem) => {
// script setuploading
const tableLoading = ref(false)
//
const refreshTimer = ref<NodeJS.Timeout | null>(null)
//
const startAutoRefresh = () => {
stopAutoRefresh() //
refreshTimer.value = setInterval(() => {
//
if (!tableLoading.value) {
fetchDocuments()
}
}, 30000) // 5
}
//
const stopAutoRefresh = () => {
if (refreshTimer.value) {
clearInterval(refreshTimer.value)
refreshTimer.value = null
}
}
// fetchDocuments
const fetchDocuments = async () => {
tableLoading.value = true
@ -1043,8 +979,8 @@ try {
difyDatasetId: datasetId.value,
parentId: currentParentId.value, // 使ID
fileName: searchKeyword.value || undefined,
orderBy: sortConfig.orderBy,
orderDirection: sortConfig.orderDirection,
orderBy: 'name',
orderDirection: 'ASC',
pageNo: pagination.current,
pageSize: pagination.size
}
@ -1127,7 +1063,7 @@ const typeMap: Record<string, 'success' | 'primary' | 'warning' | 'info' | 'dang
'preprocessing': 'info',
'indexing': 'warning',
'waiting': 'info',
'queued': 'info', // info
'queued': 'primary',
'failed': 'danger',
'completed': 'success'
}
@ -1183,8 +1119,6 @@ datasetId.value = route.params.id as string
datasetName.value = route.query.name as string
// API
await fetchDocuments()
//
startAutoRefresh()
})
//
@ -1198,25 +1132,6 @@ pagination.current = 1
fetchDocuments()
}
//
const handleSortChange = (sortInfo: any) => {
if (sortInfo.prop && sortInfo.order) {
//
const fieldMapping: Record<string, string> = {
'fileName': 'name',
'createDate': 'created_at',
'charCount': 'size'
}
sortConfig.orderBy = fieldMapping[sortInfo.prop] || sortInfo.prop
sortConfig.orderDirection = sortInfo.order === 'ascending' ? 'ASC' : 'DESC'
//
pagination.current = 1
fetchDocuments()
}
}
//
const handleOpenFolder = (row: FileItem) => {
// ID
@ -1332,6 +1247,62 @@ try {
createFolderLoading.value = false
}
}
//
const handleScroll = async (event: Event) => {
const container = event.target as HTMLElement;
if(segmentList.value.length %10 !== 0 || segmentList.value.length<10) {
isScroll.value = false
return
}
//
if (container.scrollHeight - container.scrollTop - container.clientHeight < 10) {
//
if (!segmentIsLoading.value && isScroll.value) {
loadMoreSegments();
}
}
}
//
const loadMoreSegments = async () => {
segmentIsLoading.value = true; //
try {
//
const resp = await getSegmentList({
datasetId: datasetId.value,
documentId: segmentdocId.value,
page: segmentPage.value,
pageSize: segmentPageSize.value
});
//
if (resp.data.list?.length) {
segmentList.value.push(...resp.data.list); //
segmentPage.value += 1; // 1
}
} catch (error) {
ElNotification({
title: t('vabI18n.knowledge.document.errors.previewFailed'),
message: error instanceof Error ? error.message : t('vabI18n.knowledge.document.messages.getFileContentFailed'),
type: 'error'
});
} finally {
segmentIsLoading.value = false; //
}
}
//
const handlePreviewSegment = (segment) => {
previewSegment.value = segment
previewDialogVisible.value = true
}
//
const handleClosePreviewDialog = () => {
previewDialogVisible.value = false
}
</script>
<style lang="scss" scoped>
@ -1837,67 +1808,6 @@ border-top: 1px solid #ebeef5;
//
.replace-info {
margin-bottom: 24px;
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
border-left: 4px solid #e6a23c;
.target-file-info {
h4 {
margin: 0 0 16px 0;
color: #303133;
font-size: 16px;
font-weight: 600;
}
.file-info-card {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
background: white;
border-radius: 6px;
border: 1px solid #ebeef5;
.file-icon {
width: 32px;
height: 32px;
flex-shrink: 0;
object-fit: contain;
}
.file-details {
flex: 1;
.file-name {
font-size: 14px;
font-weight: 600;
color: #303133;
margin-bottom: 4px;
}
.file-meta {
display: flex;
gap: 16px;
font-size: 12px;
color: #909399;
span {
&:not(:last-child)::after {
content: '•';
margin-left: 8px;
color: #dcdfe6;
}
}
}
}
}
}
}
//
.preview-drawer {
::v-deep .el-drawer__header {
@ -2385,7 +2295,7 @@ transform: scale(0.9) translateY(-5px);
}
}
.segment-list-container {
width: 30%;
width: 50%;
border-right: 1px solid #ebeef5;
overflow-y: auto;
padding: 16px;
@ -2408,7 +2318,7 @@ transform: scale(0.9) translateY(-5px);
background: white;
transition: all 0.2s ease;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
min-height: 100px; //
min-height: 120px; //
display: flex;
flex-direction: column;
justify-content: center; //
@ -2460,6 +2370,76 @@ transform: scale(0.9) translateY(-5px);
border-radius: 0 8px 8px 0;
}
.preview-drawer {
.el-dialog {
border-radius: 14px;
background: #0f1116; //
color: #e0e6f0;
/* 边框改深色 */
border: 1px solid rgba(0, 120, 200, 0.5); //
box-shadow:
0 12px 24px rgba(0, 0, 0, 0.5), /* 底部深色阴影 */
0 0 18px rgba(255, 255, 255, 0.08); /* 柔和白色光晕 */
overflow: hidden;
transform: translateX(40px);
opacity: 0;
transition: all 0.45s cubic-bezier(0.4, 0, 0.2, 1);
&.is-enter {
transform: translateX(0);
opacity: 1;
}
&.is-leave {
transform: translateX(40px);
opacity: 0;
}
}
/* 标题栏 */
.el-dialog__header {
padding: 14px 20px;
background: linear-gradient(90deg, #00aaff, #6a5acd);
color: #fff;
font-weight: 600;
letter-spacing: 1px;
text-shadow: 0 0 6px rgba(0, 0, 0, 0.4);
border-bottom: 1px solid rgba(255, 255, 255, 0.15);
.el-dialog__title {
font-size: 16px;
}
}
/* 内容区 */
.sgement-info {
padding: 20px;
background: rgba(255, 255, 255, 0.03);
border-radius: 10px;
border: 1px solid rgba(0, 170, 255, 0.2);
box-shadow: inset 0 0 12px rgba(0, 170, 255, 0.15);
span {
font-size: 14px;
font-weight: 500;
color: #00aaff;
}
h4 {
margin-top: 12px;
font-size: 15px;
font-weight: 600;
line-height: 1.6;
color: #4a4a4a;
}
}
}
</style>

View File

@ -2,6 +2,7 @@ package com.bjtds.brichat.controller;
import com.bjtds.brichat.entity.dto.SegmentDto;
import com.bjtds.brichat.service.dify.DocumentSegmentService;
import com.bjtds.brichat.util.PageInfoResult;
import com.bjtds.brichat.util.ResultUtils;
import lombok.extern.slf4j.Slf4j;
import org.checkerframework.checker.units.qual.C;
@ -21,9 +22,11 @@ public class DocumentSegmentController {
private DocumentSegmentService documentSegmentService;
@GetMapping("/selectSegments")
public ResultUtils SelectSegments(@RequestParam("documentId") String documentId, @RequestParam("datasetId") String datasetId) {
List<SegmentDto> segments = documentSegmentService.SelectSegments(documentId, datasetId);
segments.sort(Comparator.comparing(SegmentDto::getPosition));
return ResultUtils.success(segments);
public ResultUtils SelectSegments(@RequestParam("documentId") String documentId,
@RequestParam("datasetId") String datasetId,
@RequestParam(value = "page",required = false,defaultValue = "1") int page,
@RequestParam(value = "pageSize",required = false,defaultValue = "10") int pageSize) {
PageInfoResult<SegmentDto> pageInfoResult = documentSegmentService.SelectSegments(documentId, datasetId, page, pageSize);
return ResultUtils.pageInfo(pageInfoResult);
}
}

View File

@ -9,5 +9,5 @@ import java.util.List;
@Mapper
public interface DifyDocumentSegmentMapper {
List<SegmentDto> SelectSegmentsByDocumentIdAndDatasetId(String documentId, String datasetId);
List<SegmentDto> SelectSegmentsByDocumentIdAndDatasetId(String documentId, String datasetId, int offset, int pageSize);
}

View File

@ -1,10 +1,11 @@
package com.bjtds.brichat.service.dify;
import com.bjtds.brichat.entity.dto.SegmentDto;
import com.bjtds.brichat.util.PageInfoResult;
import java.util.List;
public interface DocumentSegmentService {
List<SegmentDto> SelectSegments(String documentId, String datasetId);
PageInfoResult<SegmentDto> SelectSegments(String documentId, String datasetId, int page, int pageSize);
}

View File

@ -3,6 +3,7 @@ package com.bjtds.brichat.service.dify.impl;
import com.bjtds.brichat.entity.dto.SegmentDto;
import com.bjtds.brichat.mapper.postgresql.DifyDocumentSegmentMapper;
import com.bjtds.brichat.service.dify.DocumentSegmentService;
import com.bjtds.brichat.util.PageInfoResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@ -15,8 +16,16 @@ public class DocumentSegmentServiceImpl implements DocumentSegmentService {
@Autowired
private DifyDocumentSegmentMapper difyDocumentSegmentMapper;
// @Override
// public List<SegmentDto> SelectSegments(String documentId, String datasetId) {
// return difyDocumentSegmentMapper.SelectSegmentsByDocumentIdAndDatasetId(documentId, datasetId);
// }
@Override
public List<SegmentDto> SelectSegments(String documentId, String datasetId) {
return difyDocumentSegmentMapper.SelectSegmentsByDocumentIdAndDatasetId(documentId, datasetId);
public PageInfoResult<SegmentDto> SelectSegments(String documentId, String datasetId, int page, int pageSize) {
int offset = (page - 1) * pageSize;
List<SegmentDto> segmentDtos = difyDocumentSegmentMapper.SelectSegmentsByDocumentIdAndDatasetId(documentId, datasetId, offset, pageSize);
return new PageInfoResult<>(segmentDtos, 0);
}
}

View File

@ -11,5 +11,7 @@
<if test="datasetId != null">
dataset_id = CAST(#{datasetId} as uuid)
</if>
order by position asc
limit #{pageSize} offset #{offset}
</select>
</mapper>