Skip to content

VbenPdfPreview PDF 预览

基于 PDF.js 的 PDF 预览组件,支持翻页、缩放、下载等功能。

基础用法

vue
<script setup lang="ts">
import { VbenPdfPreview } from '@vben/common-ui'

const pdfUrl = 'https://example.com/document.pdf'
</script>

<template>
  <VbenPdfPreview :src="pdfUrl" />
</template>

自定义高度

vue
<script setup lang="ts">
import { VbenPdfPreview } from '@vben/common-ui'

const pdfUrl = 'https://example.com/document.pdf'
</script>

<template>
  <VbenPdfPreview 
    :src="pdfUrl" 
    height="800px"
  />
</template>

显示工具栏

vue
<script setup lang="ts">
import { VbenPdfPreview } from '@vben/common-ui'

const pdfUrl = 'https://example.com/document.pdf'
</script>

<template>
  <VbenPdfPreview 
    :src="pdfUrl"
    :show-toolbar="true"
  />
</template>

API

Props

参数说明类型默认值
srcPDF 文件地址string-
height预览器高度string | number'600px'
showToolbar是否显示工具栏booleantrue
showPageNumber是否显示页码booleantrue
scale初始缩放比例number1
page初始页码number1

Events

事件名说明回调参数
loadPDF 加载完成时触发(pdf: any) => void
error加载失败时触发(error: Error) => void
pageChange页码变化时触发(page: number) => void
scaleChange缩放比例变化时触发(scale: number) => void

Methods

方法名说明参数
nextPage下一页-
prevPage上一页-
goToPage跳转到指定页(page: number) => void
zoomIn放大-
zoomOut缩小-
setScale设置缩放比例(scale: number) => void
download下载 PDF-
print打印 PDF-

完整示例

vue
<script setup lang="ts">
import { ref } from 'vue'
import { message } from 'ant-design-vue'
import { VbenPdfPreview } from '@vben/common-ui'

const pdfRef = ref()
const pdfUrl = ref('https://example.com/document.pdf')
const currentPage = ref(1)
const totalPages = ref(0)
const scale = ref(1)

const handleLoad = (pdf: any) => {
  totalPages.value = pdf.numPages
  message.success('PDF 加载成功')
}

const handleError = (error: Error) => {
  message.error(`PDF 加载失败:${error.message}`)
}

const handlePageChange = (page: number) => {
  currentPage.value = page
}

const handleScaleChange = (newScale: number) => {
  scale.value = newScale
}

const handlePrevPage = () => {
  if (currentPage.value > 1) {
    pdfRef.value?.prevPage()
  }
}

const handleNextPage = () => {
  if (currentPage.value < totalPages.value) {
    pdfRef.value?.nextPage()
  }
}

const handleGoToPage = () => {
  const page = prompt('请输入页码', currentPage.value.toString())
  if (page) {
    const pageNum = parseInt(page)
    if (pageNum >= 1 && pageNum <= totalPages.value) {
      pdfRef.value?.goToPage(pageNum)
    } else {
      message.error('页码超出范围')
    }
  }
}

const handleZoomIn = () => {
  pdfRef.value?.zoomIn()
}

const handleZoomOut = () => {
  pdfRef.value?.zoomOut()
}

const handleFitWidth = () => {
  pdfRef.value?.setScale('page-width')
}

const handleFitPage = () => {
  pdfRef.value?.setScale('page-fit')
}

const handleDownload = () => {
  pdfRef.value?.download()
}

const handlePrint = () => {
  pdfRef.value?.print()
}
</script>

<template>
  <div class="pdf-container">
    <div class="pdf-toolbar">
      <a-space>
        <!-- 翻页 -->
        <a-button-group>
          <a-button 
            :disabled="currentPage <= 1"
            @click="handlePrevPage"
          >
            上一页
          </a-button>
          <a-button @click="handleGoToPage">
            {{ currentPage }} / {{ totalPages }}
          </a-button>
          <a-button 
            :disabled="currentPage >= totalPages"
            @click="handleNextPage"
          >
            下一页
          </a-button>
        </a-button-group>
        
        <!-- 缩放 -->
        <a-button-group>
          <a-button @click="handleZoomOut">缩小</a-button>
          <a-button>{{ Math.round(scale * 100) }}%</a-button>
          <a-button @click="handleZoomIn">放大</a-button>
        </a-button-group>
        
        <!-- 适应 -->
        <a-button-group>
          <a-button @click="handleFitWidth">适应宽度</a-button>
          <a-button @click="handleFitPage">适应页面</a-button>
        </a-button-group>
        
        <!-- 操作 -->
        <a-button @click="handleDownload">下载</a-button>
        <a-button @click="handlePrint">打印</a-button>
      </a-space>
    </div>
    
    <VbenPdfPreview
      ref="pdfRef"
      :src="pdfUrl"
      height="700px"
      @load="handleLoad"
      @error="handleError"
      @page-change="handlePageChange"
      @scale-change="handleScaleChange"
    />
  </div>
</template>

<style scoped>
.pdf-container {
  border: 1px solid #d9d9d9;
  border-radius: 4px;
  overflow: hidden;
}

.pdf-toolbar {
  padding: 12px 16px;
  background: #fafafa;
  border-bottom: 1px solid #d9d9d9;
}
</style>

加载本地文件

vue
<script setup lang="ts">
import { ref } from 'vue'
import { VbenPdfPreview } from '@vben/common-ui'

const pdfUrl = ref('')

const handleFileChange = (event: Event) => {
  const file = (event.target as HTMLInputElement).files?.[0]
  if (file) {
    const reader = new FileReader()
    reader.onload = (e) => {
      pdfUrl.value = e.target?.result as string
    }
    reader.readAsDataURL(file)
  }
}
</script>

<template>
  <div>
    <input 
      type="file" 
      accept=".pdf"
      @change="handleFileChange"
    >
    
    <VbenPdfPreview 
      v-if="pdfUrl"
      :src="pdfUrl"
      height="600px"
    />
  </div>
</template>

分页显示

vue
<script setup lang="ts">
import { ref } from 'vue'
import { VbenPdfPreview } from '@vben/common-ui'

const pdfUrl = 'https://example.com/document.pdf'
const currentPage = ref(1)
const pageSize = ref(1)
</script>

<template>
  <div>
    <VbenPdfPreview 
      :src="pdfUrl"
      :page="currentPage"
      height="600px"
    />
    
    <a-pagination
      v-model:current="currentPage"
      :page-size="pageSize"
      :total="totalPages"
      style="margin-top: 16px; text-align: center"
    />
  </div>
</template>

缩略图导航

vue
<script setup lang="ts">
import { ref } from 'vue'
import { VbenPdfPreview } from '@vben/common-ui'

const pdfUrl = 'https://example.com/document.pdf'
const currentPage = ref(1)
const thumbnails = ref<string[]>([])

const handleLoad = async (pdf: any) => {
  // 生成缩略图
  for (let i = 1; i <= pdf.numPages; i++) {
    const page = await pdf.getPage(i)
    const viewport = page.getViewport({ scale: 0.2 })
    const canvas = document.createElement('canvas')
    const context = canvas.getContext('2d')
    canvas.height = viewport.height
    canvas.width = viewport.width
    
    await page.render({
      canvasContext: context,
      viewport: viewport
    }).promise
    
    thumbnails.value.push(canvas.toDataURL())
  }
}

const handleThumbnailClick = (page: number) => {
  currentPage.value = page
}
</script>

<template>
  <div class="pdf-viewer">
    <div class="thumbnail-sidebar">
      <div
        v-for="(thumbnail, index) in thumbnails"
        :key="index"
        class="thumbnail-item"
        :class="{ active: currentPage === index + 1 }"
        @click="handleThumbnailClick(index + 1)"
      >
        <img :src="thumbnail" :alt="`Page ${index + 1}`">
        <span class="page-number">{{ index + 1 }}</span>
      </div>
    </div>
    
    <div class="pdf-content">
      <VbenPdfPreview 
        :src="pdfUrl"
        :page="currentPage"
        @load="handleLoad"
      />
    </div>
  </div>
</template>

<style scoped>
.pdf-viewer {
  display: flex;
  gap: 16px;
}

.thumbnail-sidebar {
  width: 150px;
  max-height: 600px;
  overflow-y: auto;
  border: 1px solid #d9d9d9;
  border-radius: 4px;
  padding: 8px;
}

.thumbnail-item {
  position: relative;
  margin-bottom: 8px;
  cursor: pointer;
  border: 2px solid transparent;
  border-radius: 4px;
  overflow: hidden;
  transition: all 0.3s;
}

.thumbnail-item:hover,
.thumbnail-item.active {
  border-color: #1890ff;
}

.thumbnail-item img {
  width: 100%;
  display: block;
}

.page-number {
  position: absolute;
  bottom: 4px;
  right: 4px;
  padding: 2px 6px;
  background: rgba(0, 0, 0, 0.7);
  color: #fff;
  font-size: 12px;
  border-radius: 2px;
}

.pdf-content {
  flex: 1;
}
</style>

水印

vue
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { VbenPdfPreview } from '@vben/common-ui'

const pdfRef = ref()
const pdfUrl = 'https://example.com/document.pdf'

onMounted(() => {
  // 添加水印
  const addWatermark = () => {
    const canvas = pdfRef.value?.getCanvas()
    if (canvas) {
      const ctx = canvas.getContext('2d')
      ctx.font = '20px Arial'
      ctx.fillStyle = 'rgba(0, 0, 0, 0.1)'
      ctx.rotate(-20 * Math.PI / 180)
      ctx.fillText('机密文件', 100, 100)
    }
  }
  
  setTimeout(addWatermark, 1000)
})
</script>

<template>
  <VbenPdfPreview 
    ref="pdfRef"
    :src="pdfUrl"
  />
</template>

注意事项

注意

  1. PDF 文件需要支持跨域访问
  2. 大文件可能影响加载速度
  3. 建议使用 CDN 加速
  4. 注意文件版权问题
  5. PDF.js 需要正确配置 worker

提示

  • 支持键盘快捷键(PageUp/PageDown)
  • 可以自定义工具栏
  • 支持打印和下载
  • 使用 scale 属性设置初始缩放
  • 可以通过 page 属性指定初始页码

键盘快捷键

快捷键功能
PageUp上一页
PageDown下一页
Home第一页
End最后一页
+放大
-缩小
0重置缩放
Ctrl + P打印

性能优化

懒加载

vue
<script setup lang="ts">
import { ref } from 'vue'
import { VbenPdfPreview } from '@vben/common-ui'

const pdfUrl = ref('')
const loading = ref(false)

const loadPdf = async () => {
  loading.value = true
  try {
    // 异步加载 PDF
    const response = await fetch('https://example.com/document.pdf')
    const blob = await response.blob()
    pdfUrl.value = URL.createObjectURL(blob)
  } finally {
    loading.value = false
  }
}
</script>

<template>
  <div>
    <a-button @click="loadPdf" :loading="loading">
      加载 PDF
    </a-button>
    
    <VbenPdfPreview 
      v-if="pdfUrl"
      :src="pdfUrl"
    />
  </div>
</template>

分页加载

typescript
// 只加载当前页和相邻页
const loadPage = async (pageNum: number) => {
  const pagesToLoad = [pageNum - 1, pageNum, pageNum + 1]
    .filter(p => p >= 1 && p <= totalPages.value)
  
  for (const page of pagesToLoad) {
    if (!loadedPages.has(page)) {
      await renderPage(page)
      loadedPages.add(page)
    }
  }
}

相关链接

MIT License