Files
TERES_web_frontend/src/pages/chunk/document-preview.tsx
guangfei.zhao 0b97991a36 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
2025-10-16 17:21:21 +08:00

309 lines
7.9 KiB
TypeScript
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.

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<IKnowledge | null>(null);
const [documentObj, setDocumentObj] = useState<IKnowledgeFile | null>(null);
const [documentFile, setDocumentFile] = useState<Blob | null>(null);
const [fileUrl, setFileUrl] = useState<string>('');
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(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 (
<CardMedia
component="img"
image={fileUrl}
alt={documentObj?.name || '文档预览'}
sx={{
maxWidth: '100%',
maxHeight: '80vh',
objectFit: 'contain',
borderRadius: 1,
}}
/>
);
} else if (fileType === 'application/pdf') {
return (
<Box
component="iframe"
src={fileUrl}
sx={{
width: '100%',
height: '80vh',
border: 'none',
borderRadius: 1,
}}
/>
);
} else if (fileType.startsWith('text/')) {
return (
<Box
component="iframe"
src={fileUrl}
sx={{
width: '100%',
height: '80vh',
border: '1px solid',
borderColor: 'grey.300',
borderRadius: 1,
}}
/>
);
} else {
return (
<Alert severity="info" sx={{ mt: 2 }}>
线
</Alert>
);
}
};
if (loading) {
return (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '400px' }}>
<CircularProgress />
</Box>
);
}
if (error && !documentObj) {
return (
<Box sx={{ p: 3 }}>
<Alert severity="error">{error}</Alert>
<Button
startIcon={<ArrowBackIcon />}
onClick={handleGoBack}
sx={{ mt: 2 }}
>
</Button>
</Box>
);
}
return (
<Box sx={{ p: 3 }}>
{/* 面包屑导航 */}
<Breadcrumbs sx={{ mb: 2 }}>
<Link
component="button"
variant="body2"
onClick={() => navigate('/knowledge')}
sx={{ textDecoration: 'none' }}
>
</Link>
<Link
component="button"
variant="body2"
onClick={() => navigate(`/knowledge/${kb_id}`)}
sx={{ textDecoration: 'none' }}
>
{kb?.name || '知识库详情'}
</Link>
<Link
component="button"
variant="body2"
onClick={() => navigate(`/knowledge/${kb_id}/document/${doc_id}/chunks`)}
sx={{ textDecoration: 'none' }}
>
</Link>
<Typography color="text.primary"></Typography>
</Breadcrumbs>
{/* 页面标题和操作按钮 */}
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
<Box>
<Typography variant="h4" gutterBottom>
</Typography>
{documentObj && (
<Typography variant="subtitle1" color="text.secondary">
{documentObj.name}
</Typography>
)}
</Box>
<Box sx={{ display: 'flex', gap: 1 }}>
<Button
startIcon={<ArrowBackIcon />}
onClick={handleGoBack}
variant="outlined"
>
</Button>
{!fileUrl && (
<Button
startIcon={<VisibilityIcon />}
onClick={loadDocumentFile}
variant="contained"
disabled={fileLoading}
>
{fileLoading ? '加载中...' : '预览文件'}
</Button>
)}
{fileUrl && (
<Button
startIcon={<DownloadIcon />}
onClick={handleDownload}
variant="contained"
>
</Button>
)}
</Box>
</Box>
{/* 错误提示 */}
{error && (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
)}
{/* 文件加载状态 */}
{fileLoading && (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '200px' }}>
<CircularProgress />
<Typography sx={{ ml: 2 }}>...</Typography>
</Box>
)}
{/* 文件预览区域 */}
{fileUrl && (
<Paper elevation={1} sx={{ p: 2 }}>
<Card>
<CardContent>
{renderFilePreview()}
</CardContent>
</Card>
</Paper>
)}
</Box>
);
}
export default DocumentPreview;