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>
哲学管理(学)人生, 文学艺术生活, 自动(计算机学)物理(学)工作, 生物(学)化学逆境, 历史(学)测绘(学)时间, 经济(学)数学金钱(理财), 心理(学)医学情绪, 诗词美容情感, 美学建筑(学)家园, 解构建构(分析)整合学习, 智商情商(IQ、EQ)运筹(学)生存.---Geovin Du(涂聚文)