fix:view pdf ppt
This commit is contained in:
@@ -46,6 +46,7 @@
|
|||||||
"lexical": "^0.39.0",
|
"lexical": "^0.39.0",
|
||||||
"mammoth": "^1.12.0",
|
"mammoth": "^1.12.0",
|
||||||
"mermaid": "^11.12.1",
|
"mermaid": "^11.12.1",
|
||||||
|
"pdfjs-dist": "^4.4.168",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-i18next": "^15.0.0",
|
"react-i18next": "^15.0.0",
|
||||||
|
|||||||
@@ -1,10 +1,32 @@
|
|||||||
import { useState, useEffect, type FC } from 'react';
|
/*
|
||||||
import { Spin, Alert, Button, Table } from 'antd';
|
* @Description:
|
||||||
import { ReloadOutlined, DownloadOutlined } from '@ant-design/icons';
|
* @Version: 0.0.1
|
||||||
|
* @Author: yujiangping
|
||||||
|
* @Date: 2026-03-16 19:01:12
|
||||||
|
* @LastEditors: yujiangping
|
||||||
|
* @LastEditTime: 2026-03-16 19:17:47
|
||||||
|
*/
|
||||||
|
import { useState, useEffect, useRef, useCallback, type FC } from 'react';
|
||||||
|
import { Spin, Alert, Button, Table, InputNumber } from 'antd';
|
||||||
|
import {
|
||||||
|
ReloadOutlined,
|
||||||
|
DownloadOutlined,
|
||||||
|
LeftOutlined,
|
||||||
|
RightOutlined,
|
||||||
|
ZoomInOutlined,
|
||||||
|
ZoomOutOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
import RbMarkdown from '../Markdown';
|
import RbMarkdown from '../Markdown';
|
||||||
import { cookieUtils } from '@/utils/request';
|
import { cookieUtils } from '@/utils/request';
|
||||||
import mammoth from 'mammoth';
|
import mammoth from 'mammoth';
|
||||||
import * as XLSX from 'xlsx';
|
import * as XLSX from 'xlsx';
|
||||||
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
|
|
||||||
|
// 设置 pdf.js worker
|
||||||
|
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||||
|
'pdfjs-dist/build/pdf.worker.mjs',
|
||||||
|
import.meta.url,
|
||||||
|
).toString();
|
||||||
|
|
||||||
interface DocumentPreviewProps {
|
interface DocumentPreviewProps {
|
||||||
fileUrl: string;
|
fileUrl: string;
|
||||||
@@ -30,10 +52,26 @@ const DocumentPreview: FC<DocumentPreviewProps> = ({
|
|||||||
const [htmlContent, setHtmlContent] = useState<string>('');
|
const [htmlContent, setHtmlContent] = useState<string>('');
|
||||||
const [excelData, setExcelData] = useState<{ sheetName: string; data: any[][] }[]>([]);
|
const [excelData, setExcelData] = useState<{ sheetName: string; data: any[][] }[]>([]);
|
||||||
|
|
||||||
|
// PDF 状态
|
||||||
|
const [pdfDoc, setPdfDoc] = useState<pdfjsLib.PDFDocumentProxy | null>(null);
|
||||||
|
const [pdfCurrentPage, setPdfCurrentPage] = useState(1);
|
||||||
|
const [pdfTotalPages, setPdfTotalPages] = useState(0);
|
||||||
|
const [pdfScale, setPdfScale] = useState(1.5);
|
||||||
|
const pdfCanvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
const pdfRenderingRef = useRef(false);
|
||||||
|
|
||||||
|
// PPT 状态
|
||||||
|
const [pptSlides, setPptSlides] = useState<string[]>([]);
|
||||||
|
const [pptCurrentPage, setPptCurrentPage] = useState(1);
|
||||||
|
const [pptTotalPages, setPptTotalPages] = useState(0);
|
||||||
|
|
||||||
// 支持预览的文件类型
|
// 支持预览的文件类型
|
||||||
const previewableTypes = ['.pdf', '.txt', '.md', '.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp', '.doc', '.docx', '.xls', '.xlsx'];
|
const previewableTypes = [
|
||||||
// PPT 暂不支持
|
'.pdf', '.txt', '.md',
|
||||||
const downloadOnlyTypes = ['.ppt', '.pptx'];
|
'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp',
|
||||||
|
'.doc', '.docx', '.xls', '.xlsx',
|
||||||
|
'.ppt', '.pptx',
|
||||||
|
];
|
||||||
|
|
||||||
const getFileExtension = () => {
|
const getFileExtension = () => {
|
||||||
if (fileExt) {
|
if (fileExt) {
|
||||||
@@ -53,8 +91,30 @@ const DocumentPreview: FC<DocumentPreviewProps> = ({
|
|||||||
const isPdfFile = () => getFileExtension() === '.pdf';
|
const isPdfFile = () => getFileExtension() === '.pdf';
|
||||||
const isWordFile = () => ['.doc', '.docx'].includes(getFileExtension());
|
const isWordFile = () => ['.doc', '.docx'].includes(getFileExtension());
|
||||||
const isExcelFile = () => ['.xls', '.xlsx'].includes(getFileExtension());
|
const isExcelFile = () => ['.xls', '.xlsx'].includes(getFileExtension());
|
||||||
|
const isPptFile = () => ['.ppt', '.pptx'].includes(getFileExtension());
|
||||||
const isPreviewable = () => previewableTypes.includes(getFileExtension());
|
const isPreviewable = () => previewableTypes.includes(getFileExtension());
|
||||||
const isDownloadOnly = () => downloadOnlyTypes.includes(getFileExtension());
|
|
||||||
|
const getRequestUrl = (url: string) => {
|
||||||
|
if (url.includes('devapi.mem.redbearai.com')) {
|
||||||
|
const parsed = new URL(url);
|
||||||
|
return parsed.pathname;
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchFileBuffer = async (url: string): Promise<ArrayBuffer> => {
|
||||||
|
const requestUrl = getRequestUrl(url);
|
||||||
|
const response = await fetch(requestUrl, {
|
||||||
|
credentials: 'include',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${cookieUtils.get('authToken') || ''}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
return response.arrayBuffer();
|
||||||
|
};
|
||||||
|
|
||||||
const handleDownload = () => {
|
const handleDownload = () => {
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
@@ -65,73 +125,132 @@ const DocumentPreview: FC<DocumentPreviewProps> = ({
|
|||||||
document.body.removeChild(link);
|
document.body.removeChild(link);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLoad = () => {
|
|
||||||
setLoading(false);
|
|
||||||
setError(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleError = (msg?: string) => {
|
const handleError = (msg?: string) => {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setError(true);
|
setError(true);
|
||||||
if (msg) setErrorMessage(msg);
|
if (msg) setErrorMessage(msg);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRetry = () => {
|
// ========== PDF 渲染逻辑 ==========
|
||||||
|
const renderPdfPage = useCallback(async (doc: pdfjsLib.PDFDocumentProxy, pageNum: number, scale: number) => {
|
||||||
|
if (pdfRenderingRef.current || !pdfCanvasRef.current) return;
|
||||||
|
pdfRenderingRef.current = true;
|
||||||
|
try {
|
||||||
|
const page = await doc.getPage(pageNum);
|
||||||
|
const viewport = page.getViewport({ scale });
|
||||||
|
const canvas = pdfCanvasRef.current;
|
||||||
|
const context = canvas.getContext('2d');
|
||||||
|
if (!context) return;
|
||||||
|
|
||||||
|
const dpr = window.devicePixelRatio || 1;
|
||||||
|
canvas.width = viewport.width * dpr;
|
||||||
|
canvas.height = viewport.height * dpr;
|
||||||
|
canvas.style.width = `${viewport.width}px`;
|
||||||
|
canvas.style.height = `${viewport.height}px`;
|
||||||
|
context.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||||
|
|
||||||
|
await page.render({ canvasContext: context, viewport }).promise;
|
||||||
|
} finally {
|
||||||
|
pdfRenderingRef.current = false;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadPdfFile = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(false);
|
setError(false);
|
||||||
setErrorMessage('');
|
setErrorMessage('');
|
||||||
|
try {
|
||||||
|
const arrayBuffer = await fetchFileBuffer(fileUrl);
|
||||||
|
const doc = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
|
||||||
|
setPdfDoc(doc);
|
||||||
|
setPdfTotalPages(doc.numPages);
|
||||||
|
setPdfCurrentPage(1);
|
||||||
|
await renderPdfPage(doc, 1, pdfScale);
|
||||||
|
setLoading(false);
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('加载 PDF 文件失败:', err);
|
||||||
|
handleError(err.message || '加载 PDF 文件失败');
|
||||||
|
}
|
||||||
|
}, [fileUrl, pdfScale, renderPdfPage]);
|
||||||
|
|
||||||
if (isTextFile() || isMarkdownFile()) {
|
const handlePdfPageChange = async (page: number) => {
|
||||||
loadTextFile();
|
if (!pdfDoc || page < 1 || page > pdfTotalPages) return;
|
||||||
} else if (isWordFile()) {
|
setPdfCurrentPage(page);
|
||||||
loadWordFile();
|
await renderPdfPage(pdfDoc, page, pdfScale);
|
||||||
} else if (isExcelFile()) {
|
};
|
||||||
loadExcelFile();
|
|
||||||
} else {
|
const handlePdfZoom = async (delta: number) => {
|
||||||
const iframe = document.querySelector(`iframe[title="${fileName || '文档预览'}"]`) as HTMLIFrameElement;
|
const newScale = Math.max(0.5, Math.min(3, pdfScale + delta));
|
||||||
if (iframe) {
|
setPdfScale(newScale);
|
||||||
iframe.src = iframe.src;
|
if (pdfDoc) {
|
||||||
}
|
await renderPdfPage(pdfDoc, pdfCurrentPage, newScale);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ========== PPT/PPTX 预览逻辑(转 PDF 后用 pdfjs 渲染每页为图片) ==========
|
||||||
|
const loadPptFile = useCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(false);
|
||||||
|
setErrorMessage('');
|
||||||
|
try {
|
||||||
|
const arrayBuffer = await fetchFileBuffer(fileUrl);
|
||||||
|
// 尝试用 pdfjs 直接加载(某些服务端会返回转换后的 PDF)
|
||||||
|
// 如果失败,则使用 Office Online Viewer 作为 fallback
|
||||||
|
try {
|
||||||
|
const doc = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
|
||||||
|
// 成功解析为 PDF,逐页渲染为图片
|
||||||
|
const slides: string[] = [];
|
||||||
|
for (let i = 1; i <= doc.numPages; i++) {
|
||||||
|
const page = await doc.getPage(i);
|
||||||
|
const viewport = page.getViewport({ scale: 2 });
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
const context = canvas.getContext('2d');
|
||||||
|
if (!context) continue;
|
||||||
|
canvas.width = viewport.width;
|
||||||
|
canvas.height = viewport.height;
|
||||||
|
await page.render({ canvasContext: context, viewport }).promise;
|
||||||
|
slides.push(canvas.toDataURL('image/png'));
|
||||||
|
}
|
||||||
|
setPptSlides(slides);
|
||||||
|
setPptTotalPages(slides.length);
|
||||||
|
setPptCurrentPage(1);
|
||||||
|
setLoading(false);
|
||||||
|
} catch {
|
||||||
|
// 不是 PDF 格式,使用 Office Online Viewer
|
||||||
|
setPptSlides([]);
|
||||||
|
setPptTotalPages(0);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('加载 PPT 文件失败:', err);
|
||||||
|
handleError(err.message || '加载 PPT 文件失败');
|
||||||
|
}
|
||||||
|
}, [fileUrl]);
|
||||||
|
|
||||||
|
// ========== 文本/Word/Excel 加载逻辑 ==========
|
||||||
const loadTextFile = async () => {
|
const loadTextFile = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(false);
|
setError(false);
|
||||||
setErrorMessage('');
|
setErrorMessage('');
|
||||||
try {
|
try {
|
||||||
let requestUrl = fileUrl;
|
const requestUrl = getRequestUrl(fileUrl);
|
||||||
|
|
||||||
if (fileUrl.includes('devapi.mem.redbearai.com')) {
|
|
||||||
const url = new URL(fileUrl);
|
|
||||||
requestUrl = url.pathname;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(requestUrl, {
|
const response = await fetch(requestUrl, {
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${cookieUtils.get('authToken') || ''}`,
|
'Authorization': `Bearer ${cookieUtils.get('authToken') || ''}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const contentType = response.headers.get('Content-Type') || '';
|
const contentType = response.headers.get('Content-Type') || '';
|
||||||
|
|
||||||
if (contentType.startsWith('image/')) {
|
if (contentType.startsWith('image/')) {
|
||||||
handleError('文件实际是图片类型,但被标记为文本文件');
|
handleError('文件实际是图片类型,但被标记为文本文件');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
|
|
||||||
if (text.startsWith('\x89PNG') || text.startsWith('<27>PNG')) {
|
if (text.startsWith('\x89PNG') || text.startsWith('<27>PNG')) {
|
||||||
handleError('文件内容是图片,但扩展名是文本');
|
handleError('文件内容是图片,但扩展名是文本');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setTextContent(text);
|
setTextContent(text);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@@ -145,25 +264,7 @@ const DocumentPreview: FC<DocumentPreviewProps> = ({
|
|||||||
setError(false);
|
setError(false);
|
||||||
setErrorMessage('');
|
setErrorMessage('');
|
||||||
try {
|
try {
|
||||||
let requestUrl = fileUrl;
|
const arrayBuffer = await fetchFileBuffer(fileUrl);
|
||||||
|
|
||||||
if (fileUrl.includes('devapi.mem.redbearai.com')) {
|
|
||||||
const url = new URL(fileUrl);
|
|
||||||
requestUrl = url.pathname;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(requestUrl, {
|
|
||||||
credentials: 'include',
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${cookieUtils.get('authToken') || ''}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const arrayBuffer = await response.arrayBuffer();
|
|
||||||
const result = await mammoth.convertToHtml({ arrayBuffer });
|
const result = await mammoth.convertToHtml({ arrayBuffer });
|
||||||
setHtmlContent(result.value);
|
setHtmlContent(result.value);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -178,33 +279,13 @@ const DocumentPreview: FC<DocumentPreviewProps> = ({
|
|||||||
setError(false);
|
setError(false);
|
||||||
setErrorMessage('');
|
setErrorMessage('');
|
||||||
try {
|
try {
|
||||||
let requestUrl = fileUrl;
|
const arrayBuffer = await fetchFileBuffer(fileUrl);
|
||||||
|
|
||||||
if (fileUrl.includes('devapi.mem.redbearai.com')) {
|
|
||||||
const url = new URL(fileUrl);
|
|
||||||
requestUrl = url.pathname;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(requestUrl, {
|
|
||||||
credentials: 'include',
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${cookieUtils.get('authToken') || ''}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const arrayBuffer = await response.arrayBuffer();
|
|
||||||
const workbook = XLSX.read(arrayBuffer, { type: 'array' });
|
const workbook = XLSX.read(arrayBuffer, { type: 'array' });
|
||||||
|
|
||||||
const sheets = workbook.SheetNames.map(sheetName => {
|
const sheets = workbook.SheetNames.map(sheetName => {
|
||||||
const worksheet = workbook.Sheets[sheetName];
|
const worksheet = workbook.Sheets[sheetName];
|
||||||
const data = XLSX.utils.sheet_to_json(worksheet, { header: 1 }) as any[][];
|
const data = XLSX.utils.sheet_to_json(worksheet, { header: 1 }) as any[][];
|
||||||
return { sheetName, data };
|
return { sheetName, data };
|
||||||
});
|
});
|
||||||
|
|
||||||
setExcelData(sheets);
|
setExcelData(sheets);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@@ -213,40 +294,72 @@ const DocumentPreview: FC<DocumentPreviewProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleRetry = () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(false);
|
||||||
|
setErrorMessage('');
|
||||||
|
if (isTextFile() || isMarkdownFile()) loadTextFile();
|
||||||
|
else if (isWordFile()) loadWordFile();
|
||||||
|
else if (isExcelFile()) loadExcelFile();
|
||||||
|
else if (isPdfFile()) loadPdfFile();
|
||||||
|
else if (isPptFile()) loadPptFile();
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isTextFile() || isMarkdownFile()) {
|
if (isTextFile() || isMarkdownFile()) loadTextFile();
|
||||||
loadTextFile();
|
else if (isWordFile()) loadWordFile();
|
||||||
} else if (isWordFile()) {
|
else if (isExcelFile()) loadExcelFile();
|
||||||
loadWordFile();
|
else if (isPdfFile()) loadPdfFile();
|
||||||
} else if (isExcelFile()) {
|
else if (isPptFile()) loadPptFile();
|
||||||
loadExcelFile();
|
else if (isImageFile()) setLoading(false);
|
||||||
}
|
|
||||||
}, [fileUrl]);
|
}, [fileUrl]);
|
||||||
|
|
||||||
// PPT 文件只提供下载
|
// PDF 翻页/缩放后重新渲染
|
||||||
if (isDownloadOnly()) {
|
useEffect(() => {
|
||||||
return (
|
if (pdfDoc && isPdfFile()) {
|
||||||
<div className={`rb:relative rb:flex rb:items-center rb:justify-center rb:bg-gray-50 rb:rounded rb:border rb:border-gray-200 ${className}`} style={{ width, height }}>
|
renderPdfPage(pdfDoc, pdfCurrentPage, pdfScale);
|
||||||
<Alert
|
}
|
||||||
message="PowerPoint 文档预览"
|
}, [pdfCurrentPage, pdfScale, pdfDoc]);
|
||||||
description={
|
|
||||||
<div className="rb:text-center">
|
// ========== 分页控制栏组件 ==========
|
||||||
<p className="rb:mb-4">PPT 文件暂不支持在线预览,请下载后查看</p>
|
const PaginationBar = ({
|
||||||
<Button
|
currentPage,
|
||||||
type="primary"
|
totalPages,
|
||||||
icon={<DownloadOutlined />}
|
onPageChange,
|
||||||
onClick={handleDownload}
|
extraControls,
|
||||||
>
|
}: {
|
||||||
下载文件
|
currentPage: number;
|
||||||
</Button>
|
totalPages: number;
|
||||||
</div>
|
onPageChange: (page: number) => void;
|
||||||
}
|
extraControls?: React.ReactNode;
|
||||||
type="info"
|
}) => (
|
||||||
showIcon
|
<div className="rb:flex rb:items-center rb:justify-center rb:gap-3 rb:py-2 rb:px-4 rb:bg-white rb:border-t rb:border-gray-200 rb:select-none">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
icon={<LeftOutlined />}
|
||||||
|
disabled={currentPage <= 1}
|
||||||
|
onClick={() => onPageChange(currentPage - 1)}
|
||||||
|
/>
|
||||||
|
<span className="rb:text-sm rb:text-gray-600 rb:flex rb:items-center rb:gap-1">
|
||||||
|
<InputNumber
|
||||||
|
size="small"
|
||||||
|
min={1}
|
||||||
|
max={totalPages}
|
||||||
|
value={currentPage}
|
||||||
|
onChange={(val) => val && onPageChange(val)}
|
||||||
|
style={{ width: 56 }}
|
||||||
/>
|
/>
|
||||||
</div>
|
<span>/ {totalPages}</span>
|
||||||
);
|
</span>
|
||||||
}
|
<Button
|
||||||
|
size="small"
|
||||||
|
icon={<RightOutlined />}
|
||||||
|
disabled={currentPage >= totalPages}
|
||||||
|
onClick={() => onPageChange(currentPage + 1)}
|
||||||
|
/>
|
||||||
|
{extraControls}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
if (!isPreviewable()) {
|
if (!isPreviewable()) {
|
||||||
return (
|
return (
|
||||||
@@ -260,7 +373,7 @@ const DocumentPreview: FC<DocumentPreviewProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`rb:relative ${className}`} style={{ width, height }}>
|
<div className={`rb:relative rb:flex rb:flex-col ${className}`} style={{ width, height }}>
|
||||||
{loading && (
|
{loading && (
|
||||||
<div className="rb:absolute rb:inset-0 rb:flex rb:items-center rb:justify-center rb:bg-gray-50 rb:z-10">
|
<div className="rb:absolute rb:inset-0 rb:flex rb:items-center rb:justify-center rb:bg-gray-50 rb:z-10">
|
||||||
<Spin size="large" tip="加载文档预览中..." />
|
<Spin size="large" tip="加载文档预览中..." />
|
||||||
@@ -275,9 +388,7 @@ const DocumentPreview: FC<DocumentPreviewProps> = ({
|
|||||||
<div>
|
<div>
|
||||||
<p className="rb:mb-2">无法加载文档预览</p>
|
<p className="rb:mb-2">无法加载文档预览</p>
|
||||||
{errorMessage && (
|
{errorMessage && (
|
||||||
<p className="rb:text-sm rb:text-red-600 rb:mb-3">
|
<p className="rb:text-sm rb:text-red-600 rb:mb-3">错误详情:{errorMessage}</p>
|
||||||
错误详情:{errorMessage}
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
<p className="rb:text-sm rb:text-gray-600 rb:mb-3">可能的原因:</p>
|
<p className="rb:text-sm rb:text-gray-600 rb:mb-3">可能的原因:</p>
|
||||||
<ul className="rb:list-disc rb:pl-5 rb:text-sm rb:text-gray-600 rb:mb-3">
|
<ul className="rb:list-disc rb:pl-5 rb:text-sm rb:text-gray-600 rb:mb-3">
|
||||||
@@ -287,12 +398,8 @@ const DocumentPreview: FC<DocumentPreviewProps> = ({
|
|||||||
<li>网络连接问题</li>
|
<li>网络连接问题</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div className="rb:mt-4 rb:flex rb:gap-2">
|
<div className="rb:mt-4 rb:flex rb:gap-2">
|
||||||
<Button icon={<ReloadOutlined />} onClick={handleRetry}>
|
<Button icon={<ReloadOutlined />} onClick={handleRetry}>重试</Button>
|
||||||
重试
|
<Button icon={<DownloadOutlined />} onClick={handleDownload}>下载文件</Button>
|
||||||
</Button>
|
|
||||||
<Button icon={<DownloadOutlined />} onClick={handleDownload}>
|
|
||||||
下载文件
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -302,8 +409,9 @@ const DocumentPreview: FC<DocumentPreviewProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 图片预览 */}
|
||||||
{isImageFile() && !error && !loading && (
|
{isImageFile() && !error && !loading && (
|
||||||
<div className="rb:w-full rb:h-full rb:overflow-auto rb:bg-gray-50 rb:flex rb:items-center rb:justify-center">
|
<div className="rb:w-full rb:flex-1 rb:overflow-auto rb:bg-gray-50 rb:flex rb:items-center rb:justify-center">
|
||||||
<img
|
<img
|
||||||
src={fileUrl}
|
src={fileUrl}
|
||||||
alt={fileName || '图片预览'}
|
alt={fileName || '图片预览'}
|
||||||
@@ -313,22 +421,25 @@ const DocumentPreview: FC<DocumentPreviewProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Markdown 预览 */}
|
||||||
{isMarkdownFile() && !error && !loading && (
|
{isMarkdownFile() && !error && !loading && (
|
||||||
<div className="rb:w-full rb:h-full rb:overflow-auto rb:bg-white rb:p-6 rb:rounded rb:border rb:border-gray-200">
|
<div className="rb:w-full rb:flex-1 rb:overflow-auto rb:bg-white rb:p-6 rb:rounded rb:border rb:border-gray-200">
|
||||||
<RbMarkdown content={textContent} />
|
<RbMarkdown content={textContent} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 文本预览 */}
|
||||||
{isTextFile() && !error && !loading && (
|
{isTextFile() && !error && !loading && (
|
||||||
<div className="rb:w-full rb:h-full rb:overflow-auto rb:bg-white rb:p-4 rb:rounded rb:border rb:border-gray-200">
|
<div className="rb:w-full rb:flex-1 rb:overflow-auto rb:bg-white rb:p-4 rb:rounded rb:border rb:border-gray-200">
|
||||||
<pre className="rb:whitespace-pre-wrap rb:text-sm rb:text-gray-800 rb:font-mono">
|
<pre className="rb:whitespace-pre-wrap rb:text-sm rb:text-gray-800 rb:font-mono">
|
||||||
{textContent}
|
{textContent}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Word 预览 */}
|
||||||
{isWordFile() && !error && !loading && (
|
{isWordFile() && !error && !loading && (
|
||||||
<div className="rb:w-full rb:h-full rb:overflow-auto rb:bg-white rb:p-6 rb:rounded rb:border rb:border-gray-200">
|
<div className="rb:w-full rb:flex-1 rb:overflow-auto rb:bg-white rb:p-6 rb:rounded rb:border rb:border-gray-200">
|
||||||
<div
|
<div
|
||||||
className="rb:prose rb:max-w-none"
|
className="rb:prose rb:max-w-none"
|
||||||
dangerouslySetInnerHTML={{ __html: htmlContent }}
|
dangerouslySetInnerHTML={{ __html: htmlContent }}
|
||||||
@@ -336,8 +447,9 @@ const DocumentPreview: FC<DocumentPreviewProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Excel 预览 */}
|
||||||
{isExcelFile() && !error && !loading && (
|
{isExcelFile() && !error && !loading && (
|
||||||
<div className="rb:w-full rb:h-full rb:overflow-auto rb:bg-white rb:p-4 rb:rounded rb:border rb:border-gray-200">
|
<div className="rb:w-full rb:flex-1 rb:overflow-auto rb:bg-white rb:p-4 rb:rounded rb:border rb:border-gray-200">
|
||||||
{excelData.map((sheet, index) => (
|
{excelData.map((sheet, index) => (
|
||||||
<div key={index} className="rb:mb-6">
|
<div key={index} className="rb:mb-6">
|
||||||
<h3 className="rb:text-lg rb:font-semibold rb:mb-3">{sheet.sheetName}</h3>
|
<h3 className="rb:text-lg rb:font-semibold rb:mb-3">{sheet.sheetName}</h3>
|
||||||
@@ -361,17 +473,84 @@ const DocumentPreview: FC<DocumentPreviewProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* PDF 预览 - 带分页和缩放 */}
|
||||||
{isPdfFile() && !error && !loading && (
|
{isPdfFile() && !error && !loading && (
|
||||||
<iframe
|
<>
|
||||||
src={fileUrl}
|
<div className="rb:w-full rb:flex-1 rb:overflow-auto rb:bg-gray-100 rb:flex rb:justify-center rb:p-4">
|
||||||
width="100%"
|
<canvas ref={pdfCanvasRef} className="rb:shadow-lg" />
|
||||||
height="100%"
|
</div>
|
||||||
title={fileName || 'PDF 预览'}
|
{pdfTotalPages > 0 && (
|
||||||
className="rb:border-0"
|
<PaginationBar
|
||||||
style={{ border: 'none' }}
|
currentPage={pdfCurrentPage}
|
||||||
onLoad={handleLoad}
|
totalPages={pdfTotalPages}
|
||||||
onError={handleError}
|
onPageChange={handlePdfPageChange}
|
||||||
/>
|
extraControls={
|
||||||
|
<div className="rb:flex rb:items-center rb:gap-1 rb:ml-4">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
icon={<ZoomOutOutlined />}
|
||||||
|
disabled={pdfScale <= 0.5}
|
||||||
|
onClick={() => handlePdfZoom(-0.25)}
|
||||||
|
/>
|
||||||
|
<span className="rb:text-sm rb:text-gray-600 rb:min-w-[48px] rb:text-center">
|
||||||
|
{Math.round(pdfScale * 100)}%
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
icon={<ZoomInOutlined />}
|
||||||
|
disabled={pdfScale >= 3}
|
||||||
|
onClick={() => handlePdfZoom(0.25)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* PPT/PPTX 预览 */}
|
||||||
|
{isPptFile() && !error && !loading && (
|
||||||
|
<>
|
||||||
|
{pptSlides.length > 0 ? (
|
||||||
|
/* 本地渲染模式(服务端返回了可解析的格式) */
|
||||||
|
<>
|
||||||
|
<div className="rb:w-full rb:flex-1 rb:overflow-auto rb:bg-gray-100 rb:flex rb:justify-center rb:items-center rb:p-4">
|
||||||
|
<img
|
||||||
|
src={pptSlides[pptCurrentPage - 1]}
|
||||||
|
alt={`Slide ${pptCurrentPage}`}
|
||||||
|
className="rb:max-w-full rb:max-h-full rb:object-contain rb:shadow-lg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<PaginationBar
|
||||||
|
currentPage={pptCurrentPage}
|
||||||
|
totalPages={pptTotalPages}
|
||||||
|
onPageChange={(page) => {
|
||||||
|
if (page >= 1 && page <= pptTotalPages) setPptCurrentPage(page);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
/* Office Online Viewer fallback */
|
||||||
|
<div className="rb:w-full rb:flex-1 rb:flex rb:flex-col">
|
||||||
|
<iframe
|
||||||
|
src={`https://view.officeapps.live.com/op/embed.aspx?src=${encodeURIComponent(fileUrl)}`}
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
title={fileName || 'PPT 预览'}
|
||||||
|
className="rb:border-0 rb:flex-1"
|
||||||
|
style={{ border: 'none' }}
|
||||||
|
onLoad={() => setLoading(false)}
|
||||||
|
onError={() => handleError('PPT 在线预览加载失败')}
|
||||||
|
/>
|
||||||
|
<div className="rb:flex rb:items-center rb:justify-center rb:gap-3 rb:py-2 rb:px-4 rb:bg-white rb:border-t rb:border-gray-200">
|
||||||
|
<span className="rb:text-sm rb:text-gray-500">使用 Office Online 预览</span>
|
||||||
|
<Button size="small" icon={<DownloadOutlined />} onClick={handleDownload}>
|
||||||
|
下载文件
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user