PDFJS 在React中的引入 使用组件打开文件流PDF

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>
相关推荐
程序员爱钓鱼7 分钟前
Next.js SSR 项目生产部署全攻略
前端·next.js·trae
程序员爱钓鱼9 分钟前
使用Git 实现Hugo热更新部署方案(零停机、自动上线)
前端·next.js·trae
颜颜yan_24 分钟前
DevUI + Vue 3 入门实战教程:从零构建AI对话应用
前端·vue.js·人工智能
国服第二切图仔1 小时前
DevUI Design中后台产品开源前端解决方案之Carousel 走马灯组件使用指南
前端·开源
无限大61 小时前
为什么浏览器能看懂网页代码?——从HTML到渲染引擎的奇幻之旅
前端
福尔摩斯张2 小时前
Linux信号捕捉特性详解:从基础到高级实践(超详细)
linux·运维·服务器·c语言·前端·驱动开发·microsoft
2401_860319522 小时前
DevUI组件库实战:从入门到企业级应用的深度探索 ,如何快速安装DevUI
前端·前端框架
cc蒲公英2 小时前
javascript有哪些内置对象
java·前端·javascript
zhangwenwu的前端小站2 小时前
vue 对接 Dify 官方 SSE 流式响应
前端·javascript·vue.js
王林不想说话2 小时前
受控/非受控组件分析
前端·react.js·typescript