VuePDF
VuePDF 是一个Vue 3的客户端组件,它允许你在项目中灵活地渲染 PDF 页面。
基础配置
安装依赖
pnpm add @tato30/vue-pdf@1.11.51
PDF 加载显示
vue
<template>
<div class="pdf-viewer">
<VuePDF :pdf="pdf" class="pdf-full" />
</div>
</template>
<script setup lang="ts">
import { VuePDF, usePDF } from '@tato30/vue-pdf'
// 官方示例 PDF
const PDF_URL =
'https://mozilla.github.io/pdf.js/web/compressed.tracemonkey-pldi-09.pdf'
// 直接解构 usePDF 返回的 pdf
const { pdf } = usePDF(PDF_URL)
</script>
<style>
.pdf-viewer {
width: 100%;
height: 100vh;
overflow: auto;
}
.pdf-full {
width: 100%;
}
</style>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
分页
vue
<template>
<div class="pdf-wrapper">
<!-- 工具栏 -->
<div class="toolbar">
<button :disabled="currentPage <= 1" @click="prevPage">上一页</button>
<span>{{ currentPage }} / {{ pages }}</span>
<button :disabled="currentPage >= pages" @click="nextPage">下一页</button>
</div>
<!-- PDF 显示区 -->
<div class="pdf-viewer">
<VuePDF
:pdf="pdf"
:page="currentPage"
class="pdf-full"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { VuePDF, usePDF } from '@tato30/vue-pdf'
// 官方示例 PDF
const PDF_URL =
'https://mozilla.github.io/pdf.js/web/compressed.tracemonkey-pldi-09.pdf'
// usePDF 会返回 pdf 和 pages(总页数)
const { pdf, pages } = usePDF(PDF_URL)
// 当前页
const currentPage = ref(1)
/**
* 上一页
*/
const prevPage = (): void => {
if (currentPage.value > 1) {
currentPage.value--
}
}
/**
* 下一页
*/
const nextPage = (): void => {
if (currentPage.value < pages.value) {
currentPage.value++
}
}
</script>
<style>
.pdf-wrapper {
display: flex;
flex-direction: column;
height: 100vh;
}
.toolbar {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 12px;
border-bottom: 1px solid #ddd;
background: #f9f9f9;
}
.pdf-viewer {
flex: 1;
overflow: auto;
}
.pdf-full {
width: 100%;
}
</style>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
缩放
vue
<template>
<div class="pdf-wrapper">
<!-- 工具栏 -->
<div class="toolbar">
<!-- 分页 -->
<button :disabled="currentPage <= 1" @click="prevPage">上一页</button>
<span>{{ currentPage }} / {{ pages }}</span>
<button :disabled="currentPage >= pages" @click="nextPage">下一页</button>
<!-- 缩放 -->
<button @click="zoomOut">-</button>
<span>{{ Math.round(scale * 100) }}%</span>
<button @click="zoomIn">+</button>
<button @click="resetZoom">重置</button>
</div>
<!-- PDF 显示区 -->
<div class="pdf-viewer">
<VuePDF
:pdf="pdf"
:page="currentPage"
:scale="scale"
class="pdf-full"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { VuePDF, usePDF } from '@tato30/vue-pdf'
// 官方示例 PDF
const PDF_URL =
'https://mozilla.github.io/pdf.js/web/compressed.tracemonkey-pldi-09.pdf'
const { pdf, pages } = usePDF(PDF_URL)
// 当前页
const currentPage = ref(1)
// 缩放比例
const scale = ref(1)
// 缩放步长、最小/最大值
const SCALE_STEP = 0.1
const SCALE_MIN = 0.5
const SCALE_MAX = 3
/**
* 放大
*/
const zoomIn = (): void => {
if (scale.value < SCALE_MAX) {
scale.value = Math.min(SCALE_MAX, scale.value + SCALE_STEP)
}
}
/**
* 缩小
*/
const zoomOut = (): void => {
if (scale.value > SCALE_MIN) {
scale.value = Math.max(SCALE_MIN, scale.value - SCALE_STEP)
}
}
/**
* 重置缩放
*/
const resetZoom = (): void => {
scale.value = 1
}
/**
* 上一页
*/
const prevPage = (): void => {
if (currentPage.value > 1) {
currentPage.value--
}
}
/**
* 下一页
*/
const nextPage = (): void => {
if (currentPage.value < pages.value) {
currentPage.value++
}
}
</script>
<style>
.pdf-wrapper {
display: flex;
flex-direction: column;
height: 100vh;
}
.toolbar {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-bottom: 1px solid #ddd;
background: #f9f9f9;
}
.pdf-viewer {
flex: 1;
overflow: auto;
}
.pdf-full {
width: 100%;
}
</style>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
自适应容器
vue
<template>
<div class="pdf-wrapper">
<!-- 工具栏 -->
<div class="toolbar">
<!-- 分页 -->
<button :disabled="currentPage <= 1" @click="prevPage">上一页</button>
<span>{{ currentPage }} / {{ pages }}</span>
<button :disabled="currentPage >= pages" @click="nextPage">下一页</button>
<!-- 缩放 -->
<button @click="zoomOut">-</button>
<span>{{ Math.round(scale * 100) }}%</span>
<button @click="zoomIn">+</button>
<button @click="resetZoom">100%</button>
<!-- 自适应 -->
<button @click="fitWidth">适应宽度</button>
<button @click="fitHeight">适应高度</button>
</div>
<!-- PDF 容器 -->
<div ref="containerRef" class="pdf-viewer">
<VuePDF
:pdf="pdf"
:page="currentPage"
:scale="scale"
class="pdf-canvas"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, nextTick } from 'vue'
import { VuePDF, usePDF } from '@tato30/vue-pdf'
const PDF_URL =
'https://mozilla.github.io/pdf.js/web/compressed.tracemonkey-pldi-09.pdf'
const { pdf, pages } = usePDF(PDF_URL)
// 分页
const currentPage = ref(1)
// 缩放
const scale = ref(1)
const SCALE_STEP = 0.1
const SCALE_MIN = 0.3
const SCALE_MAX = 4
// PDF 容器 DOM
const containerRef = ref<HTMLDivElement | null>(null)
/**
* 放大
*/
const zoomIn = (): void => {
scale.value = Math.min(SCALE_MAX, scale.value + SCALE_STEP)
}
/**
* 缩小
*/
const zoomOut = (): void => {
scale.value = Math.max(SCALE_MIN, scale.value - SCALE_STEP)
}
/**
* 重置为 100%
*/
const resetZoom = (): void => {
scale.value = 1
}
/**
* 适应宽度
* 原理:容器宽度 / PDF 页面真实宽度
*/
const fitWidth = async (): Promise<void> => {
await nextTick()
const container = containerRef.value
if (!container) return
const canvas = container.querySelector('canvas') as HTMLCanvasElement
if (!canvas) return
const containerWidth = container.clientWidth
const pdfWidth = canvas.width
scale.value = containerWidth / pdfWidth
}
/**
* 适应高度
* 原理:容器高度 / PDF 页面真实高度
*/
const fitHeight = async (): Promise<void> => {
await nextTick()
const container = containerRef.value
if (!container) return
const canvas = container.querySelector('canvas') as HTMLCanvasElement
if (!canvas) return
const containerHeight = container.clientHeight
const pdfHeight = canvas.height
scale.value = containerHeight / pdfHeight
}
/**
* 上一页
*/
const prevPage = (): void => {
if (currentPage.value > 1) {
currentPage.value--
}
}
/**
* 下一页
*/
const nextPage = (): void => {
if (currentPage.value < pages.value) {
currentPage.value++
}
}
</script>
<style>
.pdf-wrapper {
display: flex;
flex-direction: column;
height: 100vh;
}
.toolbar {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-bottom: 1px solid #ddd;
background: #f9f9f9;
}
.pdf-viewer {
flex: 1;
overflow: auto;
position: relative;
}
/* 控制 PDF 宽度不溢出 */
.pdf-canvas {
display: block;
margin: 0 auto;
}
</style>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
文本层(可选中复制)
vue
<template>
<div class="pdf-viewer">
<VuePDF
:pdf="pdf"
text-layer
class="pdf-full"
/>
</div>
</template>
<script setup lang="ts">
import { VuePDF, usePDF } from '@tato30/vue-pdf'
import '@tato30/vue-pdf/style.css'
// 官方示例 PDF
const PDF_URL =
'https://mozilla.github.io/pdf.js/web/compressed.tracemonkey-pldi-09.pdf'
// 直接解构 usePDF 返回的 pdf
const { pdf } = usePDF(PDF_URL)
</script>
<style>
.pdf-viewer {
width: 100%;
height: 100vh;
overflow: auto;
}
.pdf-full {
width: 100%;
}
</style>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
加载完成事件
vue
<template>
<div class="pdf-viewer">
<VuePDF
:pdf="pdf"
text-layer
class="pdf-full"
@loaded="handleLoaded"
@textLoaded="handleTextLoaded"
/>
</div>
</template>
<script setup lang="ts">
import { VuePDF, usePDF } from '@tato30/vue-pdf'
import '@tato30/vue-pdf/style.css'
import type { PageViewport } from 'pdfjs-dist/types/src/display/display_utils'
// 官方示例 PDF
const PDF_URL =
'https://mozilla.github.io/pdf.js/web/compressed.tracemonkey-pldi-09.pdf'
const { pdf, pages } = usePDF(PDF_URL)
/**
* PDF 页面视口加载完成
* 这是最核心的“加载完成事件”
*/
const handleLoaded = (viewport: PageViewport): void => {
console.log('📄 PDF 已加载完成')
console.log('当前页面宽高:', viewport.width, viewport.height)
console.log('总页数:', pages.value)
}
/**
* 文本层加载完成
* 只有在开启 text-layer 时才会触发
*/
const handleTextLoaded = (): void => {
console.log('📝 文本层加载完成,可选中复制已生效')
}
</script>
<style>
.pdf-viewer {
width: 100%;
height: 100vh;
overflow: auto;
}
.pdf-full {
width: 100%;
}
</style>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53