From 0b97991a36e95187ce9c60c1b21dfed01bb2186c Mon Sep 17 00:00:00 2001 From: "guangfei.zhao" Date: Thu, 16 Oct 2025 17:21:21 +0800 Subject: [PATCH] feat(document-preview): add document preview page and improve chunk list UI - Implement new document preview page with support for various file types - Move file preview functionality from parsed-result to dedicated preview page - Enhance chunk list UI with better visual hierarchy and styling - Add document preview button in parsed-result page - Improve error handling and loading states for both pages --- .../chunk/components/ChunkListResult.tsx | 282 +++++++++++----- src/pages/chunk/document-preview.tsx | 309 ++++++++++++++++++ src/pages/chunk/parsed-result.tsx | 168 +++------- src/routes/index.tsx | 3 + 4 files changed, 553 insertions(+), 209 deletions(-) create mode 100644 src/pages/chunk/document-preview.tsx diff --git a/src/pages/chunk/components/ChunkListResult.tsx b/src/pages/chunk/components/ChunkListResult.tsx index f9261c4..a296d5c 100644 --- a/src/pages/chunk/components/ChunkListResult.tsx +++ b/src/pages/chunk/components/ChunkListResult.tsx @@ -11,7 +11,17 @@ import { Pagination, CircularProgress, Alert, + Divider, + Avatar, + IconButton, + Tooltip, } from '@mui/material'; +import { + Image as ImageIcon, + TextSnippet as TextIcon, + Visibility as VisibilityIcon, + VisibilityOff as VisibilityOffIcon, +} from '@mui/icons-material'; import type { IChunk } from '@/interfaces/database/knowledge'; interface ChunkListResultProps { @@ -115,109 +125,215 @@ function ChunkListResult(props: ChunkListResultProps) { - + {chunks.map((chunk, index) => ( - - - - - Chunk #{((page - 1) * pageSize) + index + 1} - - + + + {/* 头部信息 */} + + + + + + + Chunk #{((page - 1) * pageSize) + index + 1} + + + ID: {chunk.chunk_id} + + + + {chunk.image_id && ( + + + + + + )} : } label={chunk.available_int === 1 ? '已启用' : '未启用'} size="small" color={chunk.available_int === 1 ? 'success' : 'default'} + variant={chunk.available_int === 1 ? 'filled' : 'outlined'} /> - {chunk.image_id && ( - - )} - - {chunk.content_with_weight || '无内容'} - + - {chunk.important_kwd && chunk.important_kwd.length > 0 && ( - - - 重要关键词: + {/* 内容区域 */} + + + 内容预览 + + + + {chunk.content_with_weight || '无内容'} - - {chunk.important_kwd.map((keyword, kwdIndex) => ( - - ))} - + + + + {/* 图片显示区域 */} + {chunk.image_id && ( + + + 相关图片 + + + { + const target = e.target as HTMLImageElement; + target.style.display = 'none'; + }} + /> + )} - {chunk.question_kwd && chunk.question_kwd.length > 0 && ( - - - 问题关键词: + {/* 关键词区域 */} + {((chunk.important_kwd ?? []).length > 0 || (chunk.question_kwd ?? []).length > 0 || (chunk.tag_kwd ?? []).length > 0) && ( + + + 关键词信息 - - {chunk.question_kwd.map((keyword, kwdIndex) => ( - - ))} - - - )} - - {chunk.tag_kwd && chunk.tag_kwd.length > 0 && ( - - - 标签关键词: - - - {chunk.tag_kwd.map((keyword, kwdIndex) => ( - - ))} - + + {chunk.important_kwd && chunk.important_kwd.length > 0 && ( + + + 重要关键词 + + + {chunk.important_kwd.map((keyword, kwdIndex) => ( + + ))} + + + )} + + {chunk.question_kwd && chunk.question_kwd.length > 0 && ( + + + 问题关键词 + + + {chunk.question_kwd.map((keyword, kwdIndex) => ( + + ))} + + + )} + + {chunk.tag_kwd && chunk.tag_kwd.length > 0 && ( + + + 标签关键词 + + + {chunk.tag_kwd.map((keyword, kwdIndex) => ( + + ))} + + + )} )} + {/* 位置信息 */} {chunk.positions && chunk.positions.length > 0 && ( - - - 位置信息: {chunk.positions.length} 个位置点 - + + )} diff --git a/src/pages/chunk/document-preview.tsx b/src/pages/chunk/document-preview.tsx new file mode 100644 index 0000000..3f5d946 --- /dev/null +++ b/src/pages/chunk/document-preview.tsx @@ -0,0 +1,309 @@ +import React, { useState, useEffect } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; +import { + Box, + Typography, + Paper, + CircularProgress, + Alert, + Button, + Breadcrumbs, + Link, + Card, + CardContent, + CardMedia, +} from '@mui/material'; +import { + ArrowBack as ArrowBackIcon, + Download as DownloadIcon, + Visibility as VisibilityIcon, +} from '@mui/icons-material'; +import knowledgeService from '@/services/knowledge_service'; +import type { IKnowledge, IKnowledgeFile } from '@/interfaces/database/knowledge'; + +interface DocumentPreviewProps {} + +function DocumentPreview(props: DocumentPreviewProps) { + const { kb_id, doc_id } = useParams<{ kb_id: string; doc_id: string }>(); + const navigate = useNavigate(); + + const [kb, setKb] = useState(null); + const [documentObj, setDocumentObj] = useState(null); + const [documentFile, setDocumentFile] = useState(null); + const [fileUrl, setFileUrl] = useState(''); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [fileLoading, setFileLoading] = useState(false); + + // 获取知识库和文档信息 + useEffect(() => { + const fetchData = async () => { + if (!kb_id || !doc_id) { + setError('缺少必要的参数'); + setLoading(false); + return; + } + + try { + setLoading(true); + + // 并行获取知识库信息和文档详情 + const [kbResponse, docResponse] = await Promise.all([ + knowledgeService.getKnowledgeDetail({ kb_id }), + knowledgeService.getDocumentInfos({ doc_ids: [doc_id] }) + ]); + + if (kbResponse.data.data) { + setKb(kbResponse.data.data); + } + + if (docResponse.data.data?.length > 0) { + setDocumentObj(docResponse.data.data[0]); + } + } catch (err) { + console.error('获取数据失败:', err); + setError('获取数据失败,请稍后重试'); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [kb_id, doc_id]); + + // 异步加载文档文件 + const loadDocumentFile = async () => { + if (!doc_id || fileLoading) return; + + try { + setFileLoading(true); + setError(null); + + const fileResponse = await knowledgeService.getDocumentFile({ doc_id }); + + if (fileResponse.data instanceof Blob) { + setDocumentFile(fileResponse.data); + const url = URL.createObjectURL(fileResponse.data); + setFileUrl(url); + } else { + setError('文件格式不支持预览'); + } + } catch (err) { + console.error('获取文档文件失败:', err); + setError('获取文档文件失败,请稍后重试'); + } finally { + setFileLoading(false); + } + }; + + // 清理文件URL + useEffect(() => { + return () => { + if (fileUrl) { + URL.revokeObjectURL(fileUrl); + } + }; + }, [fileUrl]); + + // 下载文件 + const handleDownload = () => { + if (fileUrl && window.document) { + const link = window.document.createElement('a'); + link.href = fileUrl; + link.download = documentObj?.name || 'document'; + window.document.body.appendChild(link); + link.click(); + window.document.body.removeChild(link); + } + }; + + // 返回上一页 + const handleGoBack = () => { + navigate(-1); + }; + + // 渲染文件预览 + const renderFilePreview = () => { + if (!documentFile || !fileUrl) return null; + + const fileType = documentFile.type; + + if (fileType.startsWith('image/')) { + return ( + + ); + } else if (fileType === 'application/pdf') { + return ( + + ); + } else if (fileType.startsWith('text/')) { + return ( + + ); + } else { + return ( + + 此文件类型不支持在线预览,请下载后查看。 + + ); + } + }; + + if (loading) { + return ( + + + + ); + } + + if (error && !documentObj) { + return ( + + {error} + + + ); + } + + return ( + + {/* 面包屑导航 */} + + navigate('/knowledge')} + sx={{ textDecoration: 'none' }} + > + 知识库 + + navigate(`/knowledge/${kb_id}`)} + sx={{ textDecoration: 'none' }} + > + {kb?.name || '知识库详情'} + + navigate(`/knowledge/${kb_id}/document/${doc_id}/chunks`)} + sx={{ textDecoration: 'none' }} + > + 文档分块 + + 文件预览 + + + {/* 页面标题和操作按钮 */} + + + + 文件预览 + + {documentObj && ( + + {documentObj.name} + + )} + + + + + + {!fileUrl && ( + + )} + + {fileUrl && ( + + )} + + + + {/* 错误提示 */} + {error && ( + + {error} + + )} + + {/* 文件加载状态 */} + {fileLoading && ( + + + 正在加载文件... + + )} + + {/* 文件预览区域 */} + {fileUrl && ( + + + + {renderFilePreview()} + + + + )} + + ); +} + +export default DocumentPreview; \ No newline at end of file diff --git a/src/pages/chunk/parsed-result.tsx b/src/pages/chunk/parsed-result.tsx index 8263157..bc9e2b7 100644 --- a/src/pages/chunk/parsed-result.tsx +++ b/src/pages/chunk/parsed-result.tsx @@ -1,24 +1,21 @@ import React, { useState, useEffect } from 'react'; import { useSearchParams, useNavigate } from "react-router-dom"; -import { - Box, - Typography, - Breadcrumbs, - Link, - TextField, +import { + Box, + Typography, + Breadcrumbs, + Link, + TextField, InputAdornment, Paper, Alert, - Card, - CardContent, - CardMedia + Button } from "@mui/material"; -import { Search as SearchIcon, ArrowBack as ArrowBackIcon } from '@mui/icons-material'; +import { Search as SearchIcon, Visibility as VisibilityIcon } from '@mui/icons-material'; import { useChunkList } from '@/hooks/chunk-hooks'; import ChunkListResult from './components/ChunkListResult'; import knowledgeService from '@/services/knowledge_service'; import type { IKnowledge, IKnowledgeFile } from '@/interfaces/database/knowledge'; -import type { IDocumentInfo } from '@/interfaces/database/document'; function ChunkParsedResult() { const [searchParams] = useSearchParams(); @@ -28,8 +25,6 @@ function ChunkParsedResult() { const [knowledgeBase, setKnowledgeBase] = useState(null); const [document, setDocument] = useState(null); - const [documentFile, setDocumentFile] = useState(null); - const [fileUrl, setFileUrl] = useState(''); const [searchKeyword, setSearchKeyword] = useState(''); // 使用chunk列表hook @@ -69,22 +64,14 @@ function ChunkParsedResult() { setDocument(docArr[0]); } } - - // 获取文档文件 - const fileResponse = await knowledgeService.getDocumentFile({ doc_id }); - if (fileResponse.data) { - // 处理二进制文件数据 - setDocumentFile(fileResponse.data); - - // 创建文件URL用于预览 - const url = URL.createObjectURL(fileResponse.data); - setFileUrl(url); - } } catch (error) { console.error('Failed to fetch data:', error); } }; + fetchData(); + }, [kb_id, doc_id]); + // 处理搜索 const handleSearch = (keyword: string) => { setSearchKeyword(keyword); @@ -92,91 +79,12 @@ function ChunkParsedResult() { setCurrentPage(1); }; - fetchData(); - - // 清理函数,释放URL对象 - return () => { - if (fileUrl) { - URL.revokeObjectURL(fileUrl); - } - }; - }, [kb_id, doc_id]); - - // 渲染文件预览组件 - const renderFilePreview = () => { - if (!document || !fileUrl) return null; - - const fileExtension = document.name?.split('.').pop()?.toLowerCase(); - - // 图片文件预览 - if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(fileExtension || '')) { - return ( - - - - 文件预览 - - - - - ); + // 处理查看文件 + const handleViewFile = () => { + if (doc_id && kb_id) { + // 跳转到文件预览页面 + navigate(`/chunk/document-preview/${kb_id}/${doc_id}`); } - - // PDF文件预览 - if (fileExtension === 'pdf') { - return ( - - - - PDF预览 - - -