1.首先安装pdf.js,可以通过yarn 或npm下载
js
//安装最新版本
yarn add pdfjs-dist
//或 npm安装
npm install pdfjs-dist
//我使用安装指定版本,用其他的老是报错
yarn add pdfjs-dist@2.11.338
2.创建查看PDF组件
ts
import { request, useLocation } from 'umi';
import React, { useEffect, useRef, useState } from 'react';
import { getDocument } from 'pdfjs-dist';
import { GlobalWorkerOptions } from 'pdfjs-dist';
// 设置 Worker 文件路径
GlobalWorkerOptions.workerSrc =
'//cdnjs.cloudflare.com/ajax/libs/pdf.js/2.11.338/pdf.worker.min.js';
interface PdfViewerProps {
url: string; // PDF 文件的 URL
}
const PdfViewer: React.FC<PdfViewerProps> = ({ url }) => {
const [scale, setScale] = useState(2); // 当前缩放比例
const [pdf, setPdf] = useState<any>(null); // 缓存 PDF 对象
const [isLoading, setIsLoading] = useState(true); // 加载状态
const [currentPage, setCurrentPage] = useState(1); // 当前页码
const pdfContainerRef = useRef<HTMLDivElement>(null);
const canvasRefs = useRef<Map<number, HTMLCanvasElement>>(new Map()); // 缓存 Canvas 元素
const placeholderRefs = useRef<Map<number, HTMLDivElement>>(new Map()); // 占位符元素
const location = useLocation();
const queryParams = new URLSearchParams(location.search);
const pdfUrl = queryParams.get('pdfUrl');
const finalPdfUrl = pdfUrl || url; // 如果 pdfUrl 为 null,则使用默认的 url
useEffect(() => {
if (typeof window !== 'undefined') {
console.log('useEffect executed');
const fetchAndRenderPDF = async () => {
try {
setIsLoading(true);
console.log('Fetching PDF...');
// 调用后端接口获取 PDF 数据
const redata = await request('/api/system_doc/getPdfData', {
method: 'post',
params: { url: finalPdfUrl },
responseType: 'arrayBuffer',
});
// 将 ArrayBuffer 转换为 Uint8Array
const uint8Array = new Uint8Array(redata);
// 使用 PDF.js 加载 PDF 文档
const loadedPdf = await getDocument({ data: uint8Array }).promise;
setPdf(loadedPdf);
// 渲染所有页面
renderAllPages(loadedPdf, scale);
} catch (error) {
console.error('Error loading PDF:', error);
} finally {
setIsLoading(false);
}
};
fetchAndRenderPDF();
}
}, [url]);
// 渲染单页 PDF
const renderPage = async (pdf: any, pageNumber: number, scale: number) => {
try {
const page = await pdf.getPage(pageNumber);
// 获取或创建 Canvas 元素
let canvas = canvasRefs.current.get(pageNumber);
if (!canvas) {
canvas = document.createElement('canvas');
canvasRefs.current.set(pageNumber, canvas);
}
const context = canvas.getContext('2d');
if (!context) return;
// 计算新的视口
const viewport = page.getViewport({ scale });
canvas.style.width = `${viewport.width}px`; // 更新 Canvas 的样式宽度
canvas.style.height = `${viewport.height}px`; // 更新 Canvas 的样式高度
canvas.width = viewport.width; // 更新 Canvas 的实际宽度
canvas.height = viewport.height; // 更新 Canvas 的实际高度
// 清空画布
context.clearRect(0, 0, canvas.width, canvas.height);
// 渲染页面
const renderContext = {
canvasContext: context,
viewport: viewport,
};
await page.render(renderContext).promise;
// 如果容器中不存在该 Canvas,则添加到对应的占位符中
const placeholder = placeholderRefs.current.get(pageNumber);
if (placeholder && !placeholder.contains(canvas)) {
placeholder.innerHTML = ''; // 清空占位符内容
placeholder.appendChild(canvas); // 插入 Canvas
}
} catch (error) {
console.error(`Error rendering page ${pageNumber}:`, error);
}
};
// 渲染所有页面
const renderAllPages = (pdf: any, scale: number) => {
if (!pdf || !pdfContainerRef.current) return;
// 创建占位符并渲染页面
for (let pageNumber = 1; pageNumber <= pdf.numPages; pageNumber++) {
// 创建占位符
let placeholder = placeholderRefs.current.get(pageNumber);
if (!placeholder) {
placeholder = document.createElement('div');
placeholder.style.position = 'relative';
placeholderRefs.current.set(pageNumber, placeholder);
pdfContainerRef.current.appendChild(placeholder);
}
// 渲染页面
renderPage(pdf, pageNumber, scale);
}
};
// 放大
const zoomIn = () => {
setScale((prevScale) => {
const newScale = prevScale + 0.5;
console.log(`Zooming in to scale: ${newScale}`);
renderAllPages(pdf, newScale);
return newScale;
});
};
// 缩小
const zoomOut = () => {
setScale((prevScale) => {
const newScale = Math.max(0.5, prevScale - 0.5);
console.log(`Zooming out to scale: ${newScale}`);
renderAllPages(pdf, newScale);
return newScale;
});
};
// 跳转到指定页码
const goToPage = async () => {
if (!pdf) return;
// 确保页码在有效范围内
const pageNumber = Math.max(1, Math.min(currentPage, pdf.numPages));
setCurrentPage(pageNumber);
// 渲染目标页面(如果尚未渲染)
await renderPage(pdf, pageNumber, scale);
// 滚动到目标页面
const placeholder = placeholderRefs.current.get(pageNumber);
if (placeholder) {
placeholder.scrollIntoView({ behavior: 'smooth' });
}
};
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
padding: '20px',
fontFamily: 'Arial, sans-serif',
backgroundColor: '#f9f9f9',
height: '100vh', // 固定高度
overflow: 'hidden', // 禁用外层滚动条
}}
>
{/* 工具栏 */}
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginBottom: '20px',
width: '80%',
maxWidth: '1200px',
padding: '10px',
backgroundColor: '#ffffff',
borderRadius: '8px',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<button
onClick={zoomIn}
disabled={isLoading}
style={{
padding: '8px 12px',
fontSize: '14px',
border: 'none',
borderRadius: '4px',
backgroundColor: '#007bff',
color: '#fff',
cursor: 'pointer',
}}
>
放大
</button>
<button
onClick={zoomOut}
disabled={isLoading}
style={{
padding: '8px 12px',
fontSize: '14px',
border: 'none',
borderRadius: '4px',
backgroundColor: '#007bff',
color: '#fff',
cursor: 'pointer',
}}
>
缩小
</button>
<span style={{ marginLeft: '10px', fontSize: '14px' }}>
当前缩放比例: {scale.toFixed(1)}x
</span>
</div>
{/* 页数显示和跳转 */}
<div style={{ marginLeft: '20px', display: 'flex', alignItems: 'center', gap: '5px' }}>
<input
type="number"
value={currentPage}
onChange={(e) => setCurrentPage(Number(e.target.value))}
min={1}
max={pdf?.numPages || 1}
style={{
width: '60px',
padding: '5px',
fontSize: '14px',
border: '1px solid #ccc',
borderRadius: '4px',
}}
/>
<span style={{ fontSize: '14px' }}>/ {pdf?.numPages || 1}</span>
<button
onClick={goToPage}
disabled={isLoading}
style={{
padding: '5px 10px',
fontSize: '14px',
border: 'none',
borderRadius: '4px',
backgroundColor: '#28a745',
color: '#fff',
cursor: 'pointer',
}}
>
跳转
</button>
</div>
</div>
{/* 加载状态 */}
{isLoading && (
<div style={{ textAlign: 'center', fontSize: '16px', color: '#555' }}>加载中...</div>
)}
{/* PDF 容器 */}
<div
ref={pdfContainerRef}
style={{
display: 'flex',
flexDirection: 'column',
gap: '10px',
overflowY: 'auto', // 保留 PDF 容器的滚动条
flex: 1, // 动态占据剩余空间
backgroundColor: '#ffffff',
borderRadius: '8px',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
padding: '10px',
}}
/>
</div>
);
};
export default PdfViewer;
3.后端返回PDF文件流,避免跨域
前端跨域看到是请求getPdfData数据。注意请求我这里写的post
php
public function getPdfData(WeRequest $request)
{
$params = $request->param();
if (isset($params['url'])) {
$params_url = $params['url'];
// 格式化 URL
$formattedUrl = $this->formatUrl($params_url);
$data = file_get_contents($formattedUrl);
echo $data;
} else {
return WeResponse::fail();
}
}
/**
* 格式化 URL
*
* @param string $url
* @return string
*/
private function formatUrl(string $url): string
{
// 1. 将反斜杠替换为正斜杠
$formattedUrl = str_replace('\\', '/', $url);
// 2. 分割基础路径和路径段
$baseUrl = parse_url($formattedUrl, PHP_URL_SCHEME) . '://' . parse_url($formattedUrl, PHP_URL_HOST);
$path = parse_url($formattedUrl, PHP_URL_PATH);
// 3. 对路径中的特殊字符进行编码
$encodedPath = implode('/', array_map('rawurlencode', explode('/', $path)));
// 4. 拼接基础路径和编码后的路径
return $baseUrl . $encodedPath;
}
4.前端设置路由链接
ts
//config/router.ts
//PDF访问界面
{
path: '/view-pdf',
component: '../components/Common/PdfViewer',
},
链接访问
ts
<a
href={`/view-pdf?pdfUrl=${list.doc_link}`}
rel="noreferrer"
target="_blank"
style={{ color: 'blue', textDecoration: 'none', fontSize: '15px' }}
>
{text}
</a>