实现 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]的错误,但是不影响预览
相关推荐
爱分享的Hayes小朋友13 小时前
如何用post请求调用Server-Sent Events服务
前端·vue3·sse·post
知难行难1 天前
福昕阅读器高级版解决文件上传IEEE PDF eXpress字体未嵌入
pdf·express
子非吾喵1 天前
在Element Ui中支持从系统粘贴版中获取图片和PDF,Docx,Doc,PPT等文档
ui·pdf·powerpoint
有过~1 天前
WPS Office手机去广高级版
pdf·wps
幸福清风2 天前
PymuPDF4llm提取pdf文件文字、表格与图片
pdf
zhentiya2 天前
王珊数据库系统概论第六版PDF+第五版课后答案+课件
数据库·pdf
Bug从此不上门3 天前
Nuxt3之使用lighthouse性能测试及性能优化实操
前端·javascript·性能优化·vue·fetch·nuxt3
大大大反派3 天前
ONLYOFFICE 8.2深度测评:集成PDF编辑、数据可视化与AI功能的强大办公套件
人工智能·信息可视化·pdf
zhentiya3 天前
濮良贵《机械设计》第十版课后习题答案全解PDF电子版
pdf
Coisini_甜柚か3 天前
打字机效果显示
前端·vue3·antv