import React, { useEffect, useState, useRef } from 'react';
import * as pdfjsLib from 'pdfjs-dist';
// 设置 worker 路径
pdfjsLib.GlobalWorkerOptions.workerSrc = '/pdf.worker.min.js';
const PDFViewer = ({ url }) => {
const [pdf, setPdf] = useState(null);
const [currentPage, setCurrentPage] = useState(1);
const [numPages, setNumPages] = useState(0);
const [pageRendering, setPageRendering] = useState(false);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const canvasRef = useRef(null);
const linkLayerRef = useRef(null); // 添加链接层的引用
const pageCache = useRef(new Map());
const scale = useRef(1); // 添加scale引用以在不同函数间共享
// 添加水印函数
const addWatermark = (canvas, scale) => {
const ctx = canvas.getContext('2d');
const devicePixelRatio = window.devicePixelRatio || 1;
// 保存当前上下文状态
ctx.save();
// 设置水印样式
ctx.globalAlpha = 0.2; // 水印透明度
ctx.fillStyle = '#000'; // 水印颜色
// 计算基础字体大小(根据canvas宽度动态调整)
const baseFontSize = Math.min(canvas.width, canvas.height) * 0.03; // 3% 的画布大小
const fontSize = baseFontSize * devicePixelRatio;
ctx.font = `${fontSize}px Arial`;
// 水印文本
const text1 = '45380867';
const text2 = 'Jun Xiao';
// 计算水印尺寸
const text1Width = ctx.measureText(text1).width;
const text2Width = ctx.measureText(text2).width;
const lineHeight = fontSize * 1.2;
const watermarkWidth = Math.max(text1Width, text2Width);
const watermarkHeight = lineHeight * 2;
// 计算水印网格
const xGap = watermarkWidth * 2.5; // 水印之间的横向间距
const yGap = watermarkHeight * 2.5; // 水印之间的纵向间距
// 旋转角度(25度)
const angle = -25 * Math.PI / 180;
// 绘制水印网格
for (let y = -yGap; y < canvas.height + yGap; y += yGap) {
for (let x = -xGap; x < canvas.width + xGap; x += xGap) {
ctx.save();
// 移动到水印位置并旋转
ctx.translate(x, y);
ctx.rotate(angle);
// 绘制两行文本
ctx.fillText(text1, -text1Width / 2, 0);
ctx.fillText(text2, -text2Width / 2, lineHeight);
ctx.restore();
}
}
// 恢复上下文状态
ctx.restore();
};
// 添加处理链接的函数
const setupLinkLayer = (page, viewport) => {
const linkLayer = linkLayerRef.current;
if (!linkLayer) return;
// 清除旧的链接
while (linkLayer.firstChild) {
linkLayer.removeChild(linkLayer.firstChild);
}
// 获取页面的注解(包括链接)
page.getAnnotations().then(annotations => {
annotations.forEach(annotation => {
if (annotation.subtype === 'Link' && annotation.url) {
// 创建链接元素
const link = document.createElement('a');
const bounds = viewport.convertToViewportRectangle(annotation.rect);
// 设置链接样式
link.href = annotation.url;
link.target = '_blank'; // 在新标签页中打开
link.style.position = 'absolute';
link.style.left = `${Math.min(bounds[0], bounds[2])}px`;
link.style.top = `${Math.min(bounds[1], bounds[3])}px`;
link.style.width = `${Math.abs(bounds[2] - bounds[0])}px`;
link.style.height = `${Math.abs(bounds[3] - bounds[1])}px`;
link.style.cursor = 'pointer';
// 添加到链接层
linkLayer.appendChild(link);
}
});
});
};
// 初始化 PDF
useEffect(() => {
const loadPDF = async () => {
if (!url) return;
try {
setLoading(true);
setError(null);
// 创建加载任务
const loadingTask = pdfjsLib.getDocument(url);
const pdfDoc = await loadingTask.promise;
setPdf(pdfDoc);
setNumPages(pdfDoc.numPages);
} catch (error) {
console.error('Error loading PDF:', error);
setError('PDF加载失败,请稍后重试');
} finally {
setLoading(false);
}
};
loadPDF();
return () => {
// 清理缓存的页面
pageCache.current.clear();
if (pdf) {
pdf.destroy();
}
};
}, [url]);
// 渲染页面
const renderPage = async (pageNum) => {
if (pageRendering || !pdf) return;
setPageRendering(true);
try {
// 检查缓存
if (!pageCache.current.has(pageNum)) {
const page = await pdf.getPage(pageNum);
pageCache.current.set(pageNum, page);
}
const page = pageCache.current.get(pageNum);
const canvas = canvasRef.current;
const ctx = canvas.getContext('2d');
// 计算适合屏幕的缩放比例
const viewport = page.getViewport({ scale: 1 });
const devicePixelRatio = window.devicePixelRatio || 1;
const containerWidth = canvas.parentElement.clientWidth;
const scale = (containerWidth / viewport.width) * devicePixelRatio;
// 设置canvas尺寸
const scaledViewport = page.getViewport({ scale });
canvas.width = scaledViewport.width;
canvas.height = scaledViewport.height;
canvas.style.width = `${containerWidth}px`;
canvas.style.height = `${scaledViewport.height / devicePixelRatio}px`;
// 设置链接层尺寸和位置
if (linkLayerRef.current) {
linkLayerRef.current.style.width = `${containerWidth}px`;
linkLayerRef.current.style.height = `${scaledViewport.height / devicePixelRatio}px`;
}
// 渲染PDF页面
const renderContext = {
canvasContext: ctx,
viewport: scaledViewport,
enableWebGL: true,
};
await page.render(renderContext).promise;
// 设置链接
setupLinkLayer(page, scaledViewport);
// 在PDF页面渲染完成后添加水印
addWatermark(canvas, scale);
} catch (error) {
console.error('Error rendering page:', error);
setError('页面渲染失败,请刷新重试');
} finally {
setPageRendering(false);
}
};
// 页面变化时重新渲染
useEffect(() => {
renderPage(currentPage);
}, [currentPage, pdf]);
// 内存管理:清理不可见页面的缓存
useEffect(() => {
const cleanupCache = () => {
if (pageCache.current.size > 3) { // 只保留当前页面附近的几页
const pagesToKeep = new Set([
currentPage,
currentPage - 1,
currentPage + 1
]);
pageCache.current.forEach((page, pageNum) => {
if (!pagesToKeep.has(pageNum)) {
// 确保在删除缓存前释放页面资源
page.cleanup();
pageCache.current.delete(pageNum);
}
});
}
};
cleanupCache();
}, [currentPage]);
// 处理窗口大小变化
useEffect(() => {
const handleResize = () => {
renderPage(currentPage);
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, [currentPage]);
if (loading) {
return (
<div className="loading">
<div className="loading-text">PDF文件加载中...</div>
<div className="loading-spinner"></div>
</div>
);
}
if (error) {
return (
<div className="error">
<div className="error-message">{error}</div>
<button onClick={() => window.location.reload()} className="retry-button">
重试
</button>
</div>
);
}
return (
<div className="pdf-viewer">
<div className="pdf-controls">
<button
onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))}
disabled={currentPage <= 1 || pageRendering}
className="control-button"
>
上一页
</button>
<span className="page-info">{`第 ${currentPage} 页,共 ${numPages} 页`}</span>
<button
onClick={() => setCurrentPage(prev => Math.min(prev + 1, numPages))}
disabled={currentPage >= numPages || pageRendering}
className="control-button"
>
下一页
</button>
</div>
<div className="pdf-container">
<div className="canvas-container" style={{ position: 'relative' }}>
<canvas ref={canvasRef} className="pdf-canvas" />
<div
ref={linkLayerRef}
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
pointerEvents: 'none' // 允许点击穿透到链接
}}
className="link-layer"
/>
</div>
</div>
</div>
);
};
export default PDFViewer;