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
| 参数 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| src | PDF 文件地址 | string | - |
| height | 预览器高度 | string | number | '600px' |
| showToolbar | 是否显示工具栏 | boolean | true |
| showPageNumber | 是否显示页码 | boolean | true |
| scale | 初始缩放比例 | number | 1 |
| page | 初始页码 | number | 1 |
Events
| 事件名 | 说明 | 回调参数 |
|---|---|---|
| load | PDF 加载完成时触发 | (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 | - |
| 打印 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>注意事项
注意
- PDF 文件需要支持跨域访问
- 大文件可能影响加载速度
- 建议使用 CDN 加速
- 注意文件版权问题
- 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)
}
}
}