实现 Nuxt3 预览PDF文件

news/2024/11/8 19:56:47 标签: pdf, nuxt3, vue3
  1. 安装必要的库,这里使用PDF.js库
    npm install pdfjs-dist --save
  2. 为了解决跨域问题,在server/api 下 创建一个请求api, downloadFileByProxy.ts
     
    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组件
    <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导入
    import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.js';
  5. 新建pdfPreview.vue,导入两个组件
    <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. 上面用到的判断浏览器的方法
    /**
     * 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文件
    <pdf-preview
    :path="picture.originalUrl"
    >
    </pdf-preview>

待优化的问题:

  1. 为了兼容需要重复写两个组件,试过动态导入的方式行不通
  2. 控制台会报[offsetParent is not set -- cannot scroll]的错误,但是不影响预览
     

http://www.niftyadmin.cn/n/5744368.html

相关文章

前端Web用户 token 持久化

用户 token 持久化 业务背景&#xff1a;Token的有效期会持续一段时间&#xff0c;在这段时间内没有必要重复请求token&#xff0c;但是pinia本身是基于内存的管理方式&#xff0c;刷新浏览器Token会丢失&#xff0c;为了避免丢失需要配置持久化进行缓存 基础思路&#xff1a…

【数据结构】构造函数和析构函数

在一个宁静的小镇上&#xff0c;有一座神奇的玩具工厂。这个工厂每天都能制造出各种有趣的玩具。玩具工厂有两个重要的角色&#xff1a;一位是“玩具制造师”&#xff0c;另一位是“玩具清理师”。他们的工作就像我们在编程中使用的构造函数和析构函数。 ### 玩具制造师&#…

基于Springboot的学生宿舍管理系统的设计与实现-计算机毕设 附源码 26991

基于Springboot的学生宿舍管理系统的设计与实现 摘 要 学生宿舍管理系统在高校管理中具有重要的作用&#xff0c;为提高宿舍管理效率和服务质量&#xff0c;本文基于Springboot框架开发了一款学生宿舍管理系统。该系统主要分为管理员、学生用户和宿管用户三类角色&#xff0c;每…

计算机网络——SDN

分布式控制路由 集中式控制路由

搭子小程序定制开发:全新找搭子之旅

在快节奏生活下&#xff0c;年轻人的社交渠道逐渐减少&#xff0c;为了能够获得志同道合的好友&#xff0c;满足社交需求&#xff0c;找搭子成为了当下年轻人的社交潮流&#xff0c;不管出去旅游、拍照、吃饭、打游戏等都可以寻求搭子&#xff0c;获得更高的情绪价值体验。 随…

Risc-v:mhartid寄存器

简介 mhartid&#xff08;Machine Hart ID Register&#xff09;是 RISC-V 架构中的一个控制和状态寄存器&#xff08;CSR&#xff09;&#xff0c;用于存储当前硬件线程&#xff08;hart&#xff09;的标识符。 在多核处理器中&#xff0c;每个核心可能有一个或多个硬件线程&a…

pip install pynini 失败

pip install pynini 失败&#xff0c;主要错误在编译 wheel 的时候。 解决方法&#xff1a; pip install --only-binary :all: pynini 参考&#xff1a;https://github.com/kylebgorman/pynini/issues/80

Vue2——单页应用程序路由的使用

一.单页应用程序与多页应用程序之间的比较 二.单页的应用场景 系统类网站 / 内部网站 / 文档类网站 / 移动端网站 三.路由的介绍 1. 什么是路由 路由是一种映射关系 2. Vue中的路由是什么 路径和组件的映射关系 四.VueRouter的使用 5个基础步骤&#xff08;固定&#xff09; …