<template>
|
<div class="pdf-viewer-wrapper">
|
<div v-if="loading" class="pdf-loading">
|
<div class="loading-spinner"></div>
|
<p>加载中...</p>
|
</div>
|
<div v-else-if="error" class="pdf-error">
|
<p>{{ error }}</p>
|
<button @click="loadPdf" class="retry-btn">重试</button>
|
</div>
|
<div v-else class="pdf-container">
|
<div class="pdf-toolbar">
|
<button @click="zoomOut" class="toolbar-btn" :disabled="scale <= 0.5">
|
<span class="icon">−</span>
|
</button>
|
<span class="zoom-info">{{ Math.round(scale * 100) }}%</span>
|
<button @click="zoomIn" class="toolbar-btn" :disabled="scale >= 3">
|
<span class="icon">+</span>
|
</button>
|
<div class="toolbar-divider"></div>
|
<button @click="prevPage" class="toolbar-btn" :disabled="currentPage <= 1">
|
<span class="icon">‹</span>
|
</button>
|
<span class="page-info">{{ currentPage }} / {{ totalPages }}</span>
|
<button @click="nextPage" class="toolbar-btn" :disabled="currentPage >= totalPages">
|
<span class="icon">›</span>
|
</button>
|
</div>
|
<div class="pdf-content" ref="pdfContainer">
|
<canvas ref="pdfCanvas" class="pdf-canvas"></canvas>
|
</div>
|
</div>
|
</div>
|
</template>
|
|
<script>
|
import PDFJS from 'pdfjs-dist'
|
|
// 使用本地worker
|
PDFJS.GlobalWorkerOptions.workerSrc = require('pdfjs-dist/build/pdf.worker.min.js')
|
|
export default {
|
name: 'PdfViewer',
|
props: {
|
pdfUrl: {
|
type: String,
|
required: true,
|
default: ''
|
}
|
},
|
data() {
|
return {
|
loading: true,
|
error: null,
|
pdfDoc: null,
|
currentPage: 1,
|
totalPages: 0,
|
scale: 1.0,
|
renderTask: null
|
}
|
},
|
mounted() {
|
this.loadPdf()
|
},
|
watch: {
|
pdfUrl() {
|
this.loadPdf()
|
}
|
},
|
methods: {
|
async loadPdf() {
|
if (!this.pdfUrl) {
|
this.error = 'PDF地址为空'
|
this.loading = false
|
return
|
}
|
|
this.loading = true
|
this.error = null
|
|
try {
|
console.log('开始加载PDF,URL:', this.pdfUrl)
|
|
// 获取PDF数据,确保携带认证信息
|
const token = typeof window !== 'undefined' && window.localStorage ? window.localStorage.getItem('USERTOKEN') || '' : ''
|
const url = this.pdfUrl + (this.pdfUrl.indexOf('?') > -1 ? '&' : '?') + 'token=' + encodeURIComponent(token)
|
|
console.log('请求URL:', url)
|
|
const response = await fetch(url, {
|
credentials: 'include',
|
headers: {
|
'USERTOKEN': token
|
}
|
})
|
|
if (!response.ok) {
|
throw new Error('加载PDF失败: ' + response.status)
|
}
|
|
const arrayBuffer = await response.arrayBuffer()
|
console.log('PDF数据大小:', arrayBuffer.byteLength)
|
|
if (arrayBuffer.byteLength === 0) {
|
throw new Error('PDF文件为空')
|
}
|
|
// 加载PDF文档
|
const loadingTask = PDFJS.getDocument({
|
data: arrayBuffer,
|
verbosity: 0
|
})
|
|
this.pdfDoc = await loadingTask.promise
|
this.totalPages = this.pdfDoc.numPages
|
console.log('PDF加载成功,总页数:', this.totalPages)
|
|
// 计算适合H5的初始缩放比例
|
await this.calculateInitialScale()
|
|
await this.renderPage(this.currentPage)
|
this.loading = false
|
} catch (error) {
|
console.error('加载PDF失败:', error)
|
this.error = '加载PDF失败,请稍后重试'
|
this.loading = false
|
}
|
},
|
async calculateInitialScale() {
|
if (!this.pdfDoc) return
|
|
try {
|
const page = await this.pdfDoc.getPage(1)
|
const viewport = page.getViewport({ scale: 1.0 })
|
|
// 获取容器宽度(H5适配)
|
const containerWidth = this.$refs.pdfContainer ? this.$refs.pdfContainer.clientWidth : window.innerWidth - 40
|
const containerHeight = window.innerHeight - 200
|
|
// 计算适合的缩放比例
|
const scaleX = containerWidth / viewport.width
|
const scaleY = containerHeight / viewport.height
|
this.scale = Math.min(scaleX, scaleY, 1.2) // 初始缩放不超过1.2倍
|
console.log('计算初始缩放比例:', this.scale)
|
} catch (error) {
|
console.error('计算缩放比例失败:', error)
|
this.scale = 1.0
|
}
|
},
|
async renderPage(pageNum) {
|
if (!this.pdfDoc || !this.$refs.pdfCanvas) return
|
|
// 取消之前的渲染任务
|
if (this.renderTask) {
|
this.renderTask.cancel()
|
}
|
|
try {
|
const page = await this.pdfDoc.getPage(pageNum)
|
const canvas = this.$refs.pdfCanvas
|
const context = canvas.getContext('2d')
|
|
const viewport = page.getViewport({ scale: this.scale })
|
|
// 设置canvas尺寸(H5高清适配)
|
const dpr = window.devicePixelRatio || 1
|
canvas.width = viewport.width * dpr
|
canvas.height = viewport.height * dpr
|
canvas.style.width = viewport.width + 'px'
|
canvas.style.height = viewport.height + 'px'
|
|
context.scale(dpr, dpr)
|
|
const renderContext = {
|
canvasContext: context,
|
viewport: viewport
|
}
|
|
this.renderTask = page.render(renderContext)
|
await this.renderTask.promise
|
this.currentPage = pageNum
|
console.log('渲染第', pageNum, '页完成,缩放比例:', this.scale)
|
} catch (error) {
|
if (error.name !== 'RenderingCancelledException') {
|
console.error('渲染PDF页面失败:', error)
|
}
|
}
|
},
|
zoomIn() {
|
this.scale = Math.min(this.scale + 0.2, 3)
|
this.renderPage(this.currentPage)
|
},
|
zoomOut() {
|
this.scale = Math.max(this.scale - 0.2, 0.5)
|
this.renderPage(this.currentPage)
|
},
|
prevPage() {
|
if (this.currentPage > 1) {
|
this.renderPage(this.currentPage - 1)
|
}
|
},
|
nextPage() {
|
if (this.currentPage < this.totalPages) {
|
this.renderPage(this.currentPage + 1)
|
}
|
}
|
},
|
beforeDestroy() {
|
if (this.renderTask) {
|
this.renderTask.cancel()
|
}
|
}
|
}
|
</script>
|
|
<style scoped>
|
.pdf-viewer-wrapper {
|
width: 100%;
|
height: 100%;
|
background: #f5f5f5;
|
}
|
|
.pdf-loading {
|
display: flex;
|
flex-direction: column;
|
align-items: center;
|
justify-content: center;
|
height: 60vh;
|
color: #666;
|
}
|
|
.loading-spinner {
|
width: 40px;
|
height: 40px;
|
border: 4px solid #f3f3f3;
|
border-top: 4px solid #f11514;
|
border-radius: 50%;
|
animation: spin 1s linear infinite;
|
margin-bottom: 15px;
|
}
|
|
@keyframes spin {
|
0% { transform: rotate(0deg); }
|
100% { transform: rotate(360deg); }
|
}
|
|
.pdf-error {
|
display: flex;
|
flex-direction: column;
|
align-items: center;
|
justify-content: center;
|
height: 60vh;
|
color: #f56c6c;
|
padding: 20px;
|
text-align: center;
|
}
|
|
.retry-btn {
|
margin-top: 15px;
|
padding: 10px 20px;
|
background: linear-gradient(-55deg, rgb(241, 22, 20), rgb(240, 40, 37));
|
color: #fff;
|
border: none;
|
border-radius: 5px;
|
font-size: 14px;
|
cursor: pointer;
|
}
|
|
.pdf-container {
|
width: 100%;
|
height: 100%;
|
display: flex;
|
flex-direction: column;
|
background: #525252;
|
}
|
|
.pdf-toolbar {
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
padding: 10px 5px;
|
background: #fff;
|
border-bottom: 1px solid #e0e0e0;
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
position: sticky;
|
top: 0;
|
z-index: 10;
|
flex-shrink: 0;
|
}
|
|
.toolbar-btn {
|
width: 36px;
|
height: 36px;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
background: #f5f5f5;
|
border: 1px solid #e0e0e0;
|
border-radius: 4px;
|
margin: 0 3px;
|
cursor: pointer;
|
font-size: 20px;
|
color: #333;
|
transition: all 0.2s;
|
-webkit-tap-highlight-color: transparent;
|
}
|
|
.toolbar-btn:active {
|
background: #e0e0e0;
|
transform: scale(0.95);
|
}
|
|
.toolbar-btn:disabled {
|
opacity: 0.4;
|
cursor: not-allowed;
|
}
|
|
.toolbar-btn .icon {
|
line-height: 1;
|
font-weight: bold;
|
user-select: none;
|
}
|
|
.zoom-info,
|
.page-info {
|
min-width: 55px;
|
text-align: center;
|
font-size: 13px;
|
color: #666;
|
margin: 0 6px;
|
user-select: none;
|
}
|
|
.toolbar-divider {
|
width: 1px;
|
height: 24px;
|
background: #e0e0e0;
|
margin: 0 6px;
|
}
|
|
.pdf-content {
|
flex: 1;
|
overflow: auto;
|
-webkit-overflow-scrolling: touch;
|
background: #525252;
|
display: flex;
|
align-items: flex-start;
|
justify-content: center;
|
padding: 15px 10px;
|
min-height: calc(100vh - 280px);
|
}
|
|
.pdf-canvas {
|
display: block;
|
margin: 0 auto;
|
background: #fff;
|
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
max-width: 100%;
|
height: auto;
|
touch-action: pan-x pan-y pinch-zoom;
|
}
|
|
/* H5移动端优化 */
|
@media (max-width: 768px) {
|
.pdf-toolbar {
|
padding: 8px 3px;
|
}
|
|
.toolbar-btn {
|
width: 32px;
|
height: 32px;
|
margin: 0 2px;
|
font-size: 18px;
|
}
|
|
.zoom-info,
|
.page-info {
|
font-size: 12px;
|
margin: 0 4px;
|
min-width: 50px;
|
}
|
|
.pdf-content {
|
padding: 10px 5px;
|
min-height: calc(100vh - 160px);
|
}
|
}
|
|
/* 横屏适配 */
|
@media (orientation: landscape) {
|
.pdf-content {
|
min-height: calc(100vh - 140px);
|
}
|
}
|
</style>
|