实现 Nuxt3 预览PDF文件

  1. 安装必要的库,这里使用PDF.js库

    javascript 复制代码
    npm install pdfjs-dist --save
  2. 为了解决跨域问题,在server/api 下 创建一个请求api, downloadFileByProxy.ts

    TypeScript 复制代码
     
    import { defineEventHandler } from 'h3';
     
    export default defineEventHandler(async event => {
      const { filePath } =  getQuery(event);
      let matches = filePath?.match(/^https?\:\/\/([^\/?#]+)(?:[\/?#]|$)/i);
      let domain = matches && matches[1]; 
      return proxyRequest(event,`https://${domain}/`, {
        fetch: ()=>fetch(filePath),
      })
    })
  3. 支持现代浏览器,新建pdfPreviewForMordern.vue组件

    javascript 复制代码
    <script setup lang="ts">
      import { isPdf } from '~/utils/is';
      import 'pdfjs-dist/web/pdf_viewer.css';
      import * as pdfjsLib from 'pdfjs-dist';
      // import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.js'; // 旧版浏览器需要换成这个导入
      import * as pdfjsViewer from 'pdfjs-dist/web/pdf_viewer';
      import 'pdfjs-dist/build/pdf.worker.entry';
      import * as pdfjsSandbox from 'pdfjs-dist/build/pdf.sandbox.js';
      // import {  PDFDocumentProxy } from 'pdfjs-dist/types/src/display/api';
      import { debounce } from 'lodash-es';
    
      const props = defineProps({
        path: {
          type: String,
          default: '',
        },
        preview: {
          type: Boolean,
          default: true,
        },
      });
    
      const SANDBOX_BUNDLE_SRC = pdfjsSandbox;
    
      pdfjsLib.GlobalWorkerOptions.workerSrc = window.pdfjsWorker;
    
      const CMAP_URL = '/pdfjs-dist/cmaps/';
      const CMAP_PACKED = true;
      const STANDARD_FONT_DATA_URL = '/pdfjs-dist/standard_fonts/';
    
      window.pdfjsLib = pdfjsLib;
      window.pdfjsViewer = pdfjsViewer;
    
      const pdfEventBus = new pdfjsViewer.EventBus();
    
      const pdfScriptingManager = new pdfjsViewer.PDFScriptingManager({
        eventBus: pdfEventBus,
        sandboxBundleSrc: SANDBOX_BUNDLE_SRC,
      });
      const pdfLinkService = new pdfjsViewer.PDFLinkService({
        eventBus: pdfEventBus,
      });
    
      // (Optionally) enable find controller.
      const pdfFindController = new pdfjsViewer.PDFFindController({
        eventBus: pdfEventBus,
        linkService: pdfLinkService,
      });
    
      let pdfViewer: pdfjsViewer.PDFViewer | null = null;
      let pdfDocument: PDFDocumentProxy | null = null;
    
      const loading = ref<boolean>(true);
      const visible = ref<boolean>(false);
      const setVisible = (value: boolean): void => {
        if (!props.preview) {
          return;
        }
        visible.value = value;
      };
    
      let oldPath = '';
      const random = ref(Math.floor(Math.random() * 10001));
      const bufferCache = ref(null); // 使用缓存避免多次请求,可以试具体情况优化与否
    
      watch(
        () => props.path,
        async (val) => {
          if (!val || !isPdf(val)) {
            return;
          }
          setTimeout(() => {
            debounceRenderHandle();
          }, 500);
        },
        {
          immediate: true,
        },
      );
    
      const debounceRenderHandle = debounce(() => {
        initPage(props.path, `pdfjs-container-${random.value}`, 'page-height');
      }, 500);
    
      const preview = async () => {
        setVisible(true);
        if (oldPath === props.path) {
          return;
        }
        if (!props.path) {
          return;
        }
        oldPath = props.path;
        setTimeout(() => {
          initPage(props.path, `pdfjs-modal-container-${random.value}`);
        }, 500);
      };
    
      async function getFile(pdfPath: string) {
        // 为了防止跨域需要再次请求
        const { _data } = await $fetch.raw(`/api/downloadFileByProxy`,{
          method: 'get',
          params: {
            // filePath: val.split('/').pop(),
            filePath: pdfPath,
          },
        })
        let blob = _data;
        let buffer = await blob?.arrayBuffer();
        return buffer;
      }
    
      async function initPage(pdfPath: string, domId: string, zoom?: string | number) {
        if (!pdfPath) return;
        try {
          // download pdf from api to prevent CORS
          bufferCache.value = bufferCache.value || (await getFile(pdfPath));
          let container = document.getElementById(domId);
          pdfDocument = await pdfjsLib.getDocument({
            // url: pdfUrl as unknown as URL,
            data: useCloneDeep(bufferCache.value),
            cMapUrl: CMAP_URL,
            cMapPacked: CMAP_PACKED,
            standardFontDataUrl: STANDARD_FONT_DATA_URL,
          }).promise;
          pdfViewer = new pdfjsViewer.PDFViewer({
            container: container as unknown as HTMLDivElement,
            eventBus: pdfEventBus,
            annotationMode: 0,
            annotationEditorMode: 0,
            scriptingManager: pdfScriptingManager,
            linkService: pdfLinkService,
          });
          pdfScriptingManager.setDocument(pdfDocument);
          pdfScriptingManager.setViewer(pdfViewer);
          pdfLinkService.setDocument(pdfDocument);
          pdfLinkService.setViewer(pdfViewer);
    
          pdfViewer.setDocument(pdfDocument);
          pdfEventBus.on('pagesinit', () => {
            if (pdfViewer) {
              loading.value = false;
              // TODO: this code will report error, but not affect results: [offsetParent is not set -- cannot scroll]
              zoom ? pdfLinkService.setHash(`zoom=${zoom}`) : pdfLinkService.setHash(`zoom=100`);
            }
          });
        } catch {
          // Init pdf Page error
        }
      }
    </script>
    
    <template>
      <div class="w-full h-full">
        <div @click="preview" class="absolute inset-0 cursor-pointer z-[100]" v-if="preview"></div>
        <img :src="useRuntimeConfig().public.loadingPicture" class="absolute object-cover w-full h-full" v-if="loading" />
        <div :id="`pdfjs-container-${random}`" class="page-container page-thumbnail-container no-scrollbar">
          <div :id="`pdfViewer-${random}`" class="pdfViewer pdf-thumbnail-viewer"></div>
        </div>
        <a-modal v-model:visible="visible" :footer="null" width="100%" wrap-class-name="ant-full-modal">
          <template #closeIcon>
            <span class="font-semibold bg-white cursor-pointer text-litepie-primary-600 text-[16px]">
              <ms-icon path="close-icon" type="svg" :w="48" :h="48" />
            </span>
          </template>
          <div :id="`pdfjs-modal-container-${random}`" class="page-container page-modal-container">
            <div :id="`pdfModalViewer-${random}`" class="pdfViewer"></div>
          </div>
        </a-modal>
      </div>
    </template>
    
    <style lang="less">
      .ant-full-modal {
        .ant-modal {
          max-width: 100%;
          top: 0;
          padding-bottom: 0;
          margin: 0;
        }
        .ant-modal-content {
          display: flex;
          flex-direction: column;
          height: calc(100vh);
        }
        .ant-modal-body {
          flex: 1;
          padding: 0;
        }
        .ant-modal-close {
          top: 30px;
          right: 30px;
        }
      }
    </style>
    <style lang="less" scoped>
      .page-container {
        position: absolute;
        inset: 0;
        width: 100%;
        height: 100%;
        overflow: auto;
      }
      /* another way to scale pdf, still will report error 
      :deep(.pdf-thumbnail-viewer) {
        --scale-factor: 0.5 !important;
        canvas {
          width: 100% !important;
          height: 100% !important;
        }
      }
      */
    </style>
  4. 对于旧版浏览器,新建pdfPreviewForOld.vue,唯一不同的地方是需要替换pdfjsLib导入

    javascript 复制代码
    import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.js';
  5. 新建pdfPreview.vue,导入两个组件

    javascript 复制代码
    <script setup lang="ts">
      const supportedOlderBrowser = computed(() => {
        return getChromeVersion() <= 88 || isSafari();
      });
    </script>
    
    <template>
      <div>
        <!-- don't change v-if order, otherwise will report error -->
        <pdfPreviewForOld v-bind="$attrs" v-if="supportedOlderBrowser"></pdfPreviewForOld>
        <pdfPreviewForMordern v-else v-bind="$attrs"></pdfPreviewForMordern>
      </div>
    </template>
  6. 上面用到的判断浏览器的方法

    TypeScript 复制代码
    /**
     * Determine whether it is safari browser
     * @return {Boolean} true,false
     */
    export const isSafari = () => getUserAgent().indexOf('safari') > -1 && !isChrome(); 
    
    /**
     * Determine whether it is chrome browser
     * @return {Boolean} true,false
     */
    export const isChrome = () => /chrome/.test(getUserAgent()) && !/chromium/.test(getUserAgent());
    
    export const getChromeVersion = () =>{  
      let raw = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./);
      return raw ? parseInt(raw[2], 10) : false;
    }
  7. 导入后预览pdf文件

    html 复制代码
    <pdf-preview
    :path="picture.originalUrl"
    >
    </pdf-preview>

待优化的问题:

  1. 为了兼容需要重复写两个组件,试过动态导入的方式行不通
  2. 控制台会报offsetParent is not set -- cannot scroll的错误,但是不影响预览
相关推荐
2601_9654542315 分钟前
AI视频制作:券商研报怎么转口播视频:PDF 解读、配音和图表自动成片的方法
人工智能·pdf·音视频
reasonsummer10 小时前
【办公类-53-12】20261016(预测)Python模仿制作2026学年第二学期校历(excel+pdf,上下学期都包含)
pdf·excel
开开心心就好14 小时前
多份文档怎么批量打印?支持多种格式
java·服务器·开发语言·pdf·ocr·散列表·启发式算法
开开心心就好16 小时前
文件被占用删不掉?解锁工具强制删除
java·运维·服务器·开发语言·前端·pdf·ocr
BD_Marathon19 小时前
PyPDFLoader和MinerU加载pdf文件
pdf
ydjs678899-bot2 天前
如何在Java中打印pdf静默打印和打印对话框选择
java·python·pdf
SamChan902 天前
PDF 翻译后 1,250 变成 1.250,是错了吗?用 Python 核对数字格式
python·pdf·ocr·数据校验·文档翻译
开开心心就好2 天前
打印机卡住打不了?一键清理卡死打印队列
服务器·开发语言·算法·pdf·ocr·idea·visual studio code
Liu.7742 天前
Vue3 中如何使用 GET 请求传递复杂参数
vue.js·vue3
打工仔折腾 AI3 天前
在线PDF工具隐私焦虑?把BentoPDF部署到极空间NAS本地处理
人工智能·后端·python·pdf·ai agent 实战