vue3: pdf.js using typescript

npm create vite vuepdfpreview  //创建项目

npm install vue-pdf-embed
npm install vue3-pdfjs
npm install pdfjs-dist@2.16.105  

  

{
  "name": "vuepdfpreview",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vue-tsc -b && vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "concurrently": "^9.1.2",
    "express": "^5.1.0",
    "pdfjs-dist": "^2.6.347",
    "vue": "^3.5.13",
    "vue-pdf-embed": "^2.1.2",
    "vue3-pdfjs": "^0.1.6"
  },
  "devDependencies": {
    "@types/express": "^4.17.17",
    "@types/node": "^20.9.0",
    "@vitejs/plugin-vue": "^5.2.3",
    "@vue/tsconfig": "^0.7.0",
    "concurrently": "^9.1.2",
    "typescript": "~5.8.3",
    "vite": "^6.3.5",
    "vue-tsc": "^2.2.8"
  }
}

  

 

<!--
 *                                |~~~~~~~|
 *                                |       |
 *                                |       |
 *                                |       |
 *                                |       |
 *                                |       |
 *     |~.\\\_\~~~~~~~~~~~~~~xx~~~         ~~~~~~~~~~~~~~~~~~~~~/_//;~|
 *     |  \  o \_         ,XXXXX),                         _..-~ o /  |
 *     |    ~~\  ~-.     XXXXX`)))),                 _.--~~   .-~~~   |
 *      ~~~~~~~`\   ~\~~~XXX' _/ ';))     |~~~~~~..-~     _.-~ ~~~~~~~
 *               `\   ~~--`_\~\, ;;;\)__.---.~~~      _.-~
 *                 ~-.       `:;;/;; \          _..-~~
 *                    ~-._      `''        /-~-~
 *                        `\              /  /
 *                          |         ,   | |
 *                           |  '        /  |
 *                            \/;          |
 *                             ;;          |
 *                             `;   .       |
 *                             |~~~-----.....|
 *                            | \             \
 *                           | /\~~--...__    |
 *                           (|  `\       __-\|
 *                           ||    \_   /~    |
 *                           |)     \~-'      |
 *                            |      | \      '
 *                            |      |  \    :
 *                             \     |  |    |
 *                              |    )  (    )
 *                               \  /;  /\  |
 *                               |    |/   |
 *                               |    |   |
 *                                \  .'  ||
 *                                |  |  | |
 *                                (  | |  |
 *                                |   \ \ |
 *                                || o `.)|
 *                                |`\\) |
 *                                |       |
 *                                |       |
 * 
 * @Author: geovindu
 * @Date: 2025-05-08 20:54:24
 * @LastEditors: geovindu
 * @LastEditTime: 2025-05-08 22:22:28
 * @FilePath: \vue\vuepdfpreview\src\App.vue
 * @Description: geovindu
 * @lib,packpage: 
 * npm install vue-pdf-embed
 * npm install vue3-pdfjs
 *  npm install pdfjs-dist@2.16.105 
 * @IDE: vscode
 * @jslib: node 20 vue.js 3.0
 * @OS: windows10
 * @database: mysql 8.0  sql server 2019 postgreSQL 16
 * Copyright (c) geovindu 2025 by geovindu@163.com, All Rights Reserved. 
 -->



<template>
<div class="pdf-container">
    <PDFView :pdfUrl="pdfUrl" v-if="pdfUrl" />
    <div v-else >加载中...</div>
  <div>
    <a href="https://vite.dev" target="_blank">
      <img src="/vite.svg" class="logo" alt="Vite logo" />
    </a>
    <a href="https://vuejs.org/" target="_blank">
      <img src="./assets/vue.svg" class="logo vue" alt="Vue logo" />
    </a>
  </div>
  <HelloWorld msg="Vite + Vue" />
 </div>
 </template>
 <script setup lang="ts">

 import HelloWorld from './components/HelloWorld.vue'
 //import PDFView from "./components/pdfPreview.vue" // 可以
 import PDFView from "./components/pdfjs.vue"   //可以
 //import jsPdf from "http://localhost:5173/pdfs/01.pdf"
 const pdfUrl = "./pdfs/09.pdf"
 </script>

 
<style scoped>
.logo {
  height: 6em;
  padding: 1.5em;
  will-change: filter;
  transition: filter 300ms;
}
.logo:hover {
  filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.vue:hover {
  filter: drop-shadow(0 0 2em #42b883aa);
}
</style>

  

<!--
 *                        ::
 *                       :;J7, :,                        ::;7:
 *                       ,ivYi, ,                       ;LLLFS:
 *                       :iv7Yi                       :7ri;j5PL
 *                      ,:ivYLvr                    ,ivrrirrY2X,
 *                      :;r@Wwz.7r:                :ivu@kexianli.
 *                     :iL7::,:::iiirii:ii;::::,,irvF7rvvLujL7ur
 *                    ri::,:,::i:iiiiiii:i:irrv177JX7rYXqZEkvv17
 *                 ;i:, , ::::iirrririi:i:::iiir2XXvii;L8OGJr71i
 *               :,, ,,:   ,::ir@mingyi.irii:i:::j1jri7ZBOS7ivv,
 *                  ,::,    ::rv77iiiriii:iii:i::,rvLq@huhao.Li
 *              ,,      ,, ,:ir7ir::,:::i;ir:::i:i::rSGGYri712:
 *            :::  ,v7r:: ::rrv77:, ,, ,:i7rrii:::::, ir7ri7Lri
 *           ,     2OBBOi,iiir;r::        ,irriiii::,, ,iv7Luur:
 *         ,,     i78MBBi,:,:::,:,  :7FSL: ,iriii:::i::,,:rLqXv::
 *         :      iuMMP: :,:::,:ii;2GY7OBB0viiii:i:iii:i:::iJqL;::
 *        ,     ::::i   ,,,,, ::LuBBu BBBBBErii:i:i:i:i:i:i:r77ii
 *       ,       :       , ,,:::rruBZ1MBBqi, :,,,:::,::::::iiriri:
 *      ,               ,,,,::::i:  @arqiao.       ,:,, ,:::ii;i7:
 *     :,       rjujLYLi   ,,:::::,:::::::::,,   ,:i,:,,,,,::i:iii
 *     ::      BBBBBBBBB0,    ,,::: , ,:::::: ,      ,,,, ,,:::::::
 *     i,  ,  ,8BMMBBBBBBi     ,,:,,     ,,, , ,   , , , :,::ii::i::
 *     :      iZMOMOMBBM2::::::::::,,,,     ,,,,,,:,,,::::i:irr:i:::,
 *     i   ,,:;u0MBMOG1L:::i::::::  ,,,::,   ,,, ::::::i:i:iirii:i:i:
 *     :    ,iuUuuXUkFu7i:iii:i:::, :,:,: ::::::::i:i:::::iirr7iiri::
 *     :     :rk@Yizero.i:::::, ,:ii:::::::i:::::i::,::::iirrriiiri::,
 *      :      5BMBBBBBBSr:,::rv2kuii:::iii::,:i:,, , ,,:,:i@petermu.,
 *           , :r50EZ8MBBBBGOBBBZP7::::i::,:::::,: :,:,::i;rrririiii::
 *               :jujYY7LS0ujJL7r::,::i::,::::::::::::::iirirrrrrrr:ii:
 *            ,:  :@kevensun.:,:,,,::::i:i:::::,,::::::iir;ii;7v77;ii;i,
 *            ,,,     ,,:,::::::i:iiiii:i::::,, ::::iiiir@xingjief.r;7:i,
 *         , , ,,,:,,::::::::iiiiiiiiii:,:,:::::::::iiir;ri7vL77rrirri::
 *          :,, , ::::::::i:::i:::i:i::,,,,,:,::i:i:::iir;@Secbone.ii:::
 * 
 * @Author: geovindu
 * @Date: 2025-05-08 21:00:52
 * @LastEditors: geovindu
 * @LastEditTime: 2025-05-08 22:47:03
 * @FilePath: \vue\vuepdfpreview\src\components\pdfjs.vue
 * @Description: geovindu
 * @lib,packpage: 
 * 
 * @IDE: vscode
 * @jslib: node 20 vue.js 3.0
 * @OS: windows10
 * @database: mysql 8.0  sql server 2019 postgreSQL 16
 * Copyright (c) geovindu 2025 by geovindu@163.com, All Rights Reserved. 
 -->
<template>
    <div class="pdf-container">
      <div v-if="loading" class="text-center py-8">加载中...</div>
      <div v-else-if="error" class="text-center py-8 text-red-500">{{ error }}</div>
      <div v-else>
        <div class="flex justify-between items-center mb-4">
          <div class="flex space-x-2">
            <button @click="zoomIn" class="px-3 py-1 bg-blue-500 text-white rounded hover:bg-blue-600">放大</button>
            <button @click="zoomOut" class="px-3 py-1 bg-blue-500 text-white rounded hover:bg-blue-600">缩小</button>
            <button @click="downloadPDF" class="px-3 py-1 bg-green-500 text-white rounded hover:bg-green-600">下载文档</button>
          </div>
          <div class="text-center">
            第 {{ currentPage }} / {{ totalPages }} 页
            <button @click="prevPage" :disabled="currentPage <= 1" class="px-3 py-1 bg-gray-200 rounded hover:bg-gray-300">上一页</button>
            <button @click="nextPage" :disabled="currentPage >= totalPages" class="px-3 py-1 bg-gray-200 rounded hover:bg-gray-300">下一页</button>
          </div>
        </div>
        <div id="pdf-container" class="w-full h-[600px] border border-gray-300 overflow-auto">
          <canvas ref="pdfCanvas"></canvas>
        </div>
      </div>
    </div>
  </template>
  
  <script setup>
  import { ref, onMounted, reactive } from 'vue';
  import * as pdfjsLib from 'pdfjs-dist';
  
  // 设置 worker 路径
  pdfjsLib.GlobalWorkerOptions.workerSrc = '/pdfjs/pdf.worker.js';
  
  const props = defineProps({
    pdfUrl: { type: String, required: true }
  });
  
  const pdfCanvas = ref(null);
  const loading = ref(true);
  const error = ref('');
  const currentPage = ref(1);
  const totalPages = ref(0);
  const scale = ref(1.0);
  let pdfDoc = null;
  
  const renderPage = async (num) => {
    try {
      const page = await pdfDoc.getPage(num);
      const viewport = page.getViewport({ scale: scale.value });
  
      // 设置 canvas 尺寸
      pdfCanvas.value.width = viewport.width;
      pdfCanvas.value.height = viewport.height;
  
      // 渲染页面
      const renderContext = {
        canvasContext: pdfCanvas.value.getContext('2d'),
        viewport: viewport
      };
  
      await page.render(renderContext).promise;
      currentPage.value = num;
    } catch (err) {
      console.error('渲染页面失败:', err);
      error.value = `渲染失败: ${err.message}`;
    }
  };
  
  const prevPage = () => {
    if (currentPage.value > 1) {
      renderPage(currentPage.value - 1);
    }
  };
  
  const nextPage = () => {
    if (currentPage.value < totalPages.value) {
      renderPage(currentPage.value + 1);
    }
  };
  
  const zoomIn = () => {
    scale.value += 0.1;
    renderPage(currentPage.value);
  };
  
  const zoomOut = () => {
    if (scale.value > 0.2) {
      scale.value -= 0.1;
      renderPage(currentPage.value);
    }
  };
  
  const downloadPDF = () => {
    const link = document.createElement('a');
    link.href = props.pdfUrl;
    link.download = props.pdfUrl.split('/').pop();
    link.click();
  };
  
  onMounted(async () => {
    try {
      // 加载 PDF
      const loadingTask = pdfjsLib.getDocument(props.pdfUrl);
      pdfDoc = await loadingTask.promise;
      totalPages.value = pdfDoc.numPages;
  
      // 渲染第一页
      renderPage(1);
      loading.value = false;
    } catch (err) {
      console.error('加载 PDF 失败:', err);
      error.value = `加载失败: ${err.message}`;
      loading.value = false;
    }
  });
  </script>
  
  <style scoped>
  .pdf-container {
    max-width: 1000px;
    margin: 0 auto;
  }
  
  #pdf-container canvas {
    max-width: 100%;
    display: block;
    margin: 0 auto;
  }
  </style>

  

<!--
 *                        ::
 *                       :;J7, :,                        ::;7:
 *                       ,ivYi, ,                       ;LLLFS:
 *                       :iv7Yi                       :7ri;j5PL
 *                      ,:ivYLvr                    ,ivrrirrY2X,
 *                      :;r@Wwz.7r:                :ivu@kexianli.
 *                     :iL7::,:::iiirii:ii;::::,,irvF7rvvLujL7ur
 *                    ri::,:,::i:iiiiiii:i:irrv177JX7rYXqZEkvv17
 *                 ;i:, , ::::iirrririi:i:::iiir2XXvii;L8OGJr71i
 *               :,, ,,:   ,::ir@mingyi.irii:i:::j1jri7ZBOS7ivv,
 *                  ,::,    ::rv77iiiriii:iii:i::,rvLq@huhao.Li
 *              ,,      ,, ,:ir7ir::,:::i;ir:::i:i::rSGGYri712:
 *            :::  ,v7r:: ::rrv77:, ,, ,:i7rrii:::::, ir7ri7Lri
 *           ,     2OBBOi,iiir;r::        ,irriiii::,, ,iv7Luur:
 *         ,,     i78MBBi,:,:::,:,  :7FSL: ,iriii:::i::,,:rLqXv::
 *         :      iuMMP: :,:::,:ii;2GY7OBB0viiii:i:iii:i:::iJqL;::
 *        ,     ::::i   ,,,,, ::LuBBu BBBBBErii:i:i:i:i:i:i:r77ii
 *       ,       :       , ,,:::rruBZ1MBBqi, :,,,:::,::::::iiriri:
 *      ,               ,,,,::::i:  @arqiao.       ,:,, ,:::ii;i7:
 *     :,       rjujLYLi   ,,:::::,:::::::::,,   ,:i,:,,,,,::i:iii
 *     ::      BBBBBBBBB0,    ,,::: , ,:::::: ,      ,,,, ,,:::::::
 *     i,  ,  ,8BMMBBBBBBi     ,,:,,     ,,, , ,   , , , :,::ii::i::
 *     :      iZMOMOMBBM2::::::::::,,,,     ,,,,,,:,,,::::i:irr:i:::,
 *     i   ,,:;u0MBMOG1L:::i::::::  ,,,::,   ,,, ::::::i:i:iirii:i:i:
 *     :    ,iuUuuXUkFu7i:iii:i:::, :,:,: ::::::::i:i:::::iirr7iiri::
 *     :     :rk@Yizero.i:::::, ,:ii:::::::i:::::i::,::::iirrriiiri::,
 *      :      5BMBBBBBBSr:,::rv2kuii:::iii::,:i:,, , ,,:,:i@petermu.,
 *           , :r50EZ8MBBBBGOBBBZP7::::i::,:::::,: :,:,::i;rrririiii::
 *               :jujYY7LS0ujJL7r::,::i::,::::::::::::::iirirrrrrrr:ii:
 *            ,:  :@kevensun.:,:,,,::::i:i:::::,,::::::iir;ii;7v77;ii;i,
 *            ,,,     ,,:,::::::i:iiiii:i::::,, ::::iiiir@xingjief.r;7:i,
 *         , , ,,,:,,::::::::iiiiiiiiii:,:,:::::::::iiir;ri7vL77rrirri::
 *          :,, , ::::::::i:::i:::i:i::,,,,,:,::i:i:::iir;@Secbone.ii:::
 * 
 * @Author: geovindu
 * @Date: 2025-05-08 21:00:52
 * @LastEditors: geovindu
 * @LastEditTime: 2025-05-0823:20:31
 * @FilePath: \vue\vuepdfpreview\src\components\pdfPreview.vue
 * @Description: geovindu
 * @lib,packpage: 
 * 
 * @IDE: vscode
 * @jslib: node 20 vue.js 3.0
 * @OS: windows10
 * @database: mysql 8.0  sql server 2019 postgreSQL 16
 * Copyright (c) geovindu 2025 by geovindu@163.com, All Rights Reserved. 
 -->
 <template>
    <div class="pdf-container">
      <div v-if="loading" class="text-center py-8">加载中...</div>
      <div v-else-if="error" class="text-center py-8 text-red-500">{{ error }}</div>
      <div v-else>
        <!-- 控制工具栏 -->
        <div class="flex justify-between items-center mb-4">
          <div class="flex space-x-2">
            <button @click="zoomIn" class="px-3 py-1 bg-blue-500 text-white rounded hover:bg-blue-600">
              <i class="fa fa-search-plus mr-1"></i> 放大
            </button>
            <button @click="zoomOut" class="px-3 py-1 bg-blue-500 text-white rounded hover:bg-blue-600">
              <i class="fa fa-search-minus mr-1"></i> 缩小
            </button>
            <button @click="downloadPDF" class="px-3 py-1 bg-green-500 text-white rounded hover:bg-green-600">
              <i class="fa fa-download mr-1"></i> 下载文档
            </button>
          </div>
          <div class="text-center">
            缩放比例: {{ scale }}%
          </div>
        </div>
        
        <!-- PDF 容器 -->
        <div id="pdf-container" class="w-full h-[600px] border border-gray-300 overflow-auto">
          <vue-pdf-embed 
            ref="pdfEmbedRef"
            :source="state.source" 
            :page="state.pageNum" 
            :scale="scale / 100" 
            textLayer 
          />
        </div>
        
        <!-- 页码控制 -->
        <div class="mt-4 text-center">
          第 {{ state.pageNum }} / {{ state.numPages }} 页
          <button @click="prevPage" :disabled="state.pageNum <= 1" class="px-3 py-1 bg-gray-200 rounded hover:bg-gray-300 mx-2">
            上一页
          </button>
          <button @click="nextPage" :disabled="state.pageNum >= state.numPages" class="px-3 py-1 bg-gray-200 rounded hover:bg-gray-300 mx-2">
            下一页
          </button>
        </div>
      </div>
    </div>
  </template>
  
  <script setup>
  import { reactive, onMounted, ref } from 'vue';
  import VuePdfEmbed from 'vue-pdf-embed';
  import { createLoadingTask } from 'vue3-pdfjs';
  
  const props = defineProps({
    pdfUrl: {
      type: String,
      required: true
    }
  });
  
  const pdfEmbedRef = ref(null);
  const state = reactive({
    source: props.pdfUrl,
    pageNum: 1,
    numPages: 0
  });
  
  const loading = ref(true);
  const error = ref('');
  const scale = ref(100); // 初始缩放比例为 100%
  
  // 加载 PDF
  onMounted(() => {
    const loadingTask = createLoadingTask(state.source);
    
    loadingTask.promise
      .then((pdf) => {
        console.log('PDF 加载成功,总页数:', pdf.numPages);
        state.numPages = pdf.numPages;
        loading.value = false;
      })
      .catch((err) => {
        console.error('PDF 加载失败:', err);
        error.value = `加载失败: ${err.message}`;
        loading.value = false;
      });
  });
  
  // 翻页控制
  const prevPage = () => {
    if (state.pageNum > 1) {
      state.pageNum--;
    }
  };
  
  const nextPage = () => {
    if (state.pageNum < state.numPages) {
      state.pageNum++;
    }
  };
  
  // 缩放控制
  const zoomIn = () => {
    scale.value = Math.min(scale.value + 25, 400); // 最大缩放 400%
  };
  
  const zoomOut = () => {
    scale.value = Math.max(scale.value - 25, 50); // 最小缩放 50%
  };
  
  // 下载 PDF
  const downloadPDF = () => {
    try {
      // 尝试使用 vue-pdf-embed 提供的下载功能
      if (pdfEmbedRef.value && pdfEmbedRef.value.download) {
        pdfEmbedRef.value.download();
      } else {
        //  fallback 方法
        const link = document.createElement('a');
        link.href = props.pdfUrl;
        link.download = props.pdfUrl.split('/').pop() || 'document.pdf';
        link.click();
      }
    } catch (e) {
      console.error('下载失败:', e);
      error.value = '下载失败,请尝试右键另存为';
      
      // 最终 fallback: 直接打开 PDF URL
      window.open(props.pdfUrl, '_blank');
    }
  };
  </script>
  
  <style scoped>
  .pdf-container {
    max-width: 1000px;
    margin: 0 auto;
  }
  
  #pdf-container canvas {
    max-width: 100%;
    display: block;
    margin: 0 auto;
  }
  </style>

  

 server.js

const cors = require('cors');
app.use(cors());q
const express = require('express');
const path = require('path');
const app = express();
const port = 3000;

// 静态文件服务
app.use(express.static(path.join(__dirname, 'public')));
app.use('/pdfjs', express.static(path.join(__dirname, 'node_modules/pdfjs-dist/build')));
app.use('/pdfjs/web', express.static(path.join(__dirname, 'node_modules/pdfjs-dist/web')));

// 假设 PDF 文件位于 public/pdf 目录下
app.get('/pdfs/:filename', (req, res) => {
  res.sendFile(path.join(__dirname, 'punpm blic/pdf', req.params.filename));
});

app.listen(port, () => {
  console.log(`服务器运行在 http://localhost:${port}`);
});

  

main.ts

import { createApp } from 'vue'
import VuePdfEmbed from "vue-pdf-embed"
import { createLoadingTask } from "vue3-pdfjs"
import './style.css'
import App from './App.vue'

createApp(App).mount('#app')

  

index.html:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite + Vue + TS</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>

  

 

 

 

 

 

 

posted @ 2025-05-09 13:26  ®Geovin Du Dream Park™  阅读(34)  评论(0)    收藏  举报