npm install pdfjs-dist
# 或者使用 yarn
yarn add pdfjs-dist
# 或者使用 pnpm
pnpm add pdfjs-dist
npm install --save-dev @types/pdfjs-dist
# 或者
npm install @types/pdfjs-dist
<!-- src/components/PdfViewer.vue -->
<template>
<div class="pdf-viewer">
<div class="controls">
<button @click="prevPage" :disabled="currentPage <= 1">上一页</button>
<span>第 {{ currentPage }} 页 / 共 {{ totalPages }} 页</span>
<button @click="nextPage" :disabled="currentPage >= totalPages">下一页</button>
<input
type="number"
v-model.number="pageInput"
@change="goToPage"
min="1"
:max="totalPages"
>
<button @click="zoomIn">放大</button>
<button @click="zoomOut">缩小</button>
<span>缩放: {{ scale.toFixed(2) }}</span>
</div>
<div class="canvas-container" ref="containerRef">
<canvas ref="canvasRef"></canvas>
</div>
<div v-if="loading" class="loading">加载中...</div>
<div v-if="error" class="error">{{ error }}</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch } from 'vue'
import * as pdfjsLib from 'pdfjs-dist'
import type { PDFDocumentProxy } from 'pdfjs-dist/types/src/pdf'
// 设置 worker
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs',
import.meta.url
).toString()
interface Props {
url: string
}
const props = defineProps<Props>()
// Refs
const canvasRef = ref<HTMLCanvasElement | null>(null)
const containerRef = ref<HTMLElement | null>(null)
const pdfDoc = ref<PDFDocumentProxy | null>(null)
const currentPage = ref(1)
const totalPages = ref(0)
const scale = ref(1.5)
const loading = ref(false)
const error = ref<string | null>(null)
const pageInput = ref(1)
// 渲染页面
const renderPage = async (pageNum: number) => {
if (!pdfDoc.value || !canvasRef.value) return
try {
loading.value = true
const page = await pdfDoc.value.getPage(pageNum)
const viewport = page.getViewport({ scale: scale.value })
const canvas = canvasRef.value
const context = canvas.getContext('2d')
if (!context) return
// 设置 Canvas 尺寸
canvas.height = viewport.height
canvas.width = viewport.width
// 渲染 PDF 页面
const renderContext = {
canvasContext: context,
viewport: viewport
}
await page.render(renderContext).promise
currentPage.value = pageNum
pageInput.value = pageNum
loading.value = false
} catch (err) {
error.value = `渲染页面失败: ${err}`
loading.value = false
}
}
// 加载 PDF 文档
const loadPDF = async () => {
try {
loading.value = true
error.value = null
const loadingTask = pdfjsLib.getDocument(props.url)
pdfDoc.value = await loadingTask.promise
totalPages.value = pdfDoc.value.numPages
await renderPage(1)
} catch (err) {
error.value = `加载PDF失败: ${err}`
loading.value = false
}
}
// 页面导航
const prevPage = () => {
if (currentPage.value > 1) {
renderPage(currentPage.value - 1)
}
}
const nextPage = () => {
if (pdfDoc.value && currentPage.value < pdfDoc.value.numPages) {
renderPage(currentPage.value + 1)
}
}
const goToPage = () => {
const pageNum = Math.max(1, Math.min(pageInput.value, totalPages.value))
renderPage(pageNum)
}
// 缩放功能
const zoomIn = () => {
scale.value += 0.25
renderPage(currentPage.value)
}
const zoomOut = () => {
if (scale.value > 0.5) {
scale.value -= 0.25
renderPage(currentPage.value)
}
}
// 监听 URL 变化
watch(() => props.url, () => {
loadPDF()
})
// 生命周期
onMounted(() => {
loadPDF()
})
onUnmounted(() => {
if (pdfDoc.value) {
pdfDoc.value.destroy()
}
})
</script>
<style scoped>
.pdf-viewer {
max-width: 100%;
margin: 0 auto;
}
.controls {
display: flex;
gap: 10px;
align-items: center;
margin-bottom: 20px;
flex-wrap: wrap;
padding: 10px;
background: #f5f5f5;
border-radius: 4px;
}
.canvas-container {
overflow: auto;
border: 1px solid #ddd;
background: #fafafa;
max-height: 80vh;
}
canvas {
display: block;
margin: 0 auto;
}
.loading, .error {
padding: 20px;
text-align: center;
font-size: 16px;
}
.loading {
color: #666;
}
.error {
color: #f44336;
}
input[type="number"] {
width: 60px;
padding: 5px;
border: 1px solid #ddd;
border-radius: 4px;
}
button {
padding: 8px 16px;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:disabled {
background: #ccc;
cursor: not-allowed;
}
button:hover:not(:disabled) {
background: #0056b3;
}
</style>
<!-- src/App.vue 或父组件 -->
<template>
<div class="app">
<h1>PDF 查看器</h1>
<!-- 使用本地 PDF 文件 -->
<PdfViewer :url="localPdfUrl" />
<!-- 或者使用远程 URL -->
<!-- <PdfViewer :url="'https://example.com/document.pdf'" /> -->
</div>
</template>
<script setup lang="ts">
import PdfViewer from './components/PdfViewer.vue'
import { ref } from 'vue'
// 本地文件需要放在 public 目录或通过 import 引入
const localPdfUrl = ref('/sample.pdf') // public/sample.pdf
// 或者使用 import
// const localPdfUrl = ref(new URL('./assets/sample.pdf', import.meta.url).href)
</script>
<!-- src/components/AdvancedPdfViewer.vue -->
<template>
<div class="advanced-pdf-viewer">
<div class="sidebar" v-if="showThumbnails">
<div
v-for="page in thumbnails"
:key="page.pageNum"
:class="['thumbnail', { active: page.pageNum === currentPage }]"
@click="renderPage(page.pageNum)"
>
<canvas :ref="el => setThumbnailRef(el, page.pageNum)"></canvas>
<div class="page-number">{{ page.pageNum }}</div>
</div>
</div>
<div class="main-content">
<PdfViewer
ref="pdfViewerRef"
:url="url"
@page-change="onPageChange"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import PdfViewer from './PdfViewer.vue'
import * as pdfjsLib from 'pdfjs-dist'
interface Props {
url: string
showThumbnails?: boolean
}
const props = withDefaults(defineProps<Props>(), {
showThumbnails: true
})
const pdfViewerRef = ref()
const thumbnails = ref<Array<{ pageNum: number }>>([])
const currentPage = ref(1)
const thumbnailCanvases = ref<Record<number, HTMLCanvasElement>>({})
const setThumbnailRef = (el: HTMLCanvasElement | null, pageNum: number) => {
if (el) {
thumbnailCanvases.value[pageNum] = el
}
}
const onPageChange = (page: number) => {
currentPage.value = page
}
// 生成缩略图
const generateThumbnails = async () => {
try {
const loadingTask = pdfjsLib.getDocument(props.url)
const pdfDoc = await loadingTask.promise
thumbnails.value = Array.from(
{ length: pdfDoc.numPages },
(_, i) => ({ pageNum: i + 1 })
)
// 批量渲染缩略图
await Promise.all(
thumbnails.value.map(async (thumbnail) => {
const page = await pdfDoc.getPage(thumbnail.pageNum)
const viewport = page.getViewport({ scale: 0.2 })
const canvas = thumbnailCanvases.value[thumbnail.pageNum]
if (canvas) {
const context = canvas.getContext('2d')
if (context) {
canvas.height = viewport.height
canvas.width = viewport.width
await page.render({
canvasContext: context,
viewport: viewport
}).promise
}
}
})
)
} catch (error) {
console.error('生成缩略图失败:', error)
}
}
onMounted(() => {
if (props.showThumbnails) {
generateThumbnails()
}
})
</script>
<style scoped>
.advanced-pdf-viewer {
display: flex;
height: 100vh;
}
.sidebar {
width: 200px;
overflow-y: auto;
border-right: 1px solid #ddd;
padding: 10px;
background: #f8f9fa;
}
.main-content {
flex: 1;
overflow: auto;
}
.thumbnail {
margin-bottom: 10px;
cursor: pointer;
border: 2px solid transparent;
border-radius: 4px;
overflow: hidden;
transition: all 0.3s;
}
.thumbnail:hover {
border-color: #007bff;
}
.thumbnail.active {
border-color: #007bff;
box-shadow: 0 0 5px rgba(0, 123, 255, 0.5);
}
.thumbnail canvas {
width: 100%;
height: auto;
display: block;
}
.page-number {
text-align: center;
padding: 5px;
background: rgba(0, 0, 0, 0.1);
font-size: 12px;
}
</style>
// src/types/pdf.d.ts
declare module 'pdfjs-dist' {
export interface PDFPageProxy {
getViewport(params: { scale: number; rotation?: number }): any
render(params: any): any
}
export interface PDFDocumentProxy {
numPages: number
getPage(pageNumber: number): Promise<PDFPageProxy>
destroy(): void
}
export const GlobalWorkerOptions: {
workerSrc: string
}
export function getDocument(src: string): {
promise: Promise<PDFDocumentProxy>
}
}
// src/composables/usePdfViewer.ts
import { ref, computed } from 'vue'
import * as pdfjsLib from 'pdfjs-dist'
export function usePdfViewer() {
const pdfDocument = ref<pdfjsLib.PDFDocumentProxy | null>(null)
const currentPage = ref(1)
const totalPages = ref(0)
const scale = ref(1.5)
const isLoading = ref(false)
const error = ref<string | null>(null)
// 加载 PDF
const loadPdf = async (url: string) => {
try {
isLoading.value = true
error.value = null
const loadingTask = pdfjsLib.getDocument(url)
pdfDocument.value = await loadingTask.promise
totalPages.value = pdfDocument.value.numPages
return pdfDocument.value
} catch (err) {
error.value = `加载 PDF 失败: ${err}`
throw err
} finally {
isLoading.value = false
}
}
// 渲染页面到 Canvas
const renderPageToCanvas = async (
pageNum: number,
canvas: HTMLCanvasElement,
options?: {
scale?: number
rotation?: number
}
) => {
if (!pdfDocument.value) {
throw new Error('PDF 文档未加载')
}
try {
isLoading.value = true
const page = await pdfDocument.value.getPage(pageNum)
const viewport = page.getViewport({
scale: options?.scale || scale.value,
rotation: options?.rotation || 0
})
const context = canvas.getContext('2d')
if (!context) {
throw new Error('无法获取 Canvas 上下文')
}
// 设置 Canvas 尺寸
canvas.height = viewport.height
canvas.width = viewport.width
// 渲染
await page.render({
canvasContext: context,
viewport
}).promise
currentPage.value = pageNum
} catch (err) {
error.value = `渲染页面失败: ${err}`
throw err
} finally {
isLoading.value = false
}
}
// 页面导航
const goToPage = (pageNum: number) => {
const page = Math.max(1, Math.min(pageNum, totalPages.value))
currentPage.value = page
return page
}
const nextPage = () => goToPage(currentPage.value + 1)
const prevPage = () => goToPage(currentPage.value - 1)
// 缩放
const zoom = (factor: number) => {
scale.value = Math.max(0.25, Math.min(5, scale.value + factor))
return scale.value
}
const zoomIn = () => zoom(0.25)
const zoomOut = () => zoom(-0.25)
// 清理
const destroy = () => {
if (pdfDocument.value) {
pdfDocument.value.destroy()
pdfDocument.value = null
}
}
return {
// 状态
pdfDocument,
currentPage,
totalPages,
scale,
isLoading,
error,
// 计算属性
hasNextPage: computed(() => currentPage.value < totalPages.value),
hasPrevPage: computed(() => currentPage.value > 1),
// 方法
loadPdf,
renderPageToCanvas,
goToPage,
nextPage,
prevPage,
zoomIn,
zoomOut,
destroy
}
}
// vite.config.ts 或 vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
optimizeDeps: {
include: ['pdfjs-dist']
}
})
// vue.config.js
module.exports = {
configureWebpack: {
resolve: {
alias: {
'pdfjs-dist/build/pdf.worker': 'pdfjs-dist/build/pdf.worker.min.js'
}
}
}
}
<!-- src/views/PdfDemo.vue -->
<template>
<div class="pdf-demo">
<div class="toolbar">
<input type="file" @change="handleFileUpload" accept=".pdf" />
<button @click="loadSamplePdf">加载示例PDF</button>
</div>
<div v-if="pdfUrl">
<AdvancedPdfViewer :url="pdfUrl" />
</div>
<div v-else class="placeholder">
请选择或上传 PDF 文件
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import AdvancedPdfViewer from '@/components/AdvancedPdfViewer.vue'
const pdfUrl = ref<string>('')
// 处理文件上传
const handleFileUpload = (event: Event) => {
const input = event.target as HTMLInputElement
if (input.files && input.files[0]) {
const file = input.files[0]
pdfUrl.value = URL.createObjectURL(file)
}
}
// 加载示例 PDF
const loadSamplePdf = () => {
// 可以从 public 目录或远程 URL 加载
pdfUrl.value = '/sample.pdf' // public/sample.pdf
// 或者
// pdfUrl.value = 'https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf'
}
</script>
// 在 main.ts 或组件中设置
import { GlobalWorkerOptions } from 'pdfjs-dist'
// Vite 项目
GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs',
import.meta.url
).toString()
// Webpack 项目
GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjsLib.version}/pdf.worker.min.js`
// 或
GlobalWorkerOptions.workerSrc = require('pdfjs-dist/build/pdf.worker.min.js')
确保安装了正确的类型声明:
// package.json
{
"devDependencies": {
"@types/pdfjs-dist": "^2.x.x"
}
}
这样就完成了完整的 PDF.js 在 Vue3 + TypeScript 项目中的集成。你可以根据具体需求调整和扩展功能。