使用pdfjs-dist预览pdf,实现预加载,滚动条翻页。pdfjs的版本很重要,换了好多版本,终于有一个能用的
node 20.18.1
"pdfjs-dist": "^2.2.228",
vue页面代码如下
<template>
<div v-loading="loading">
<div class="fixed-toolbar">
<div class="flex flex-direction">
<div class="mb4">
<el-button @click="onClose()" size="mini"
type="warning">返 回</el-button>
</div>
<div class="mb4">
<el-button @click="switchViewMode()" size="mini" type="success">切换模式</el-button>
</div>
<div class="mb4">
<el-button @click="scalBig()" size="mini"
type="primary">放 大</el-button>
</div>
<!-- <div>
<el-button class="mb4" @click="renderPdf(1)" size="mini" type="primary">默认</el-button>
</div> -->
<div>
<el-button @click="scalSmall()" size="mini"
type="primary">缩 小</el-button>
</div>
<el-dropdown size="mini" trigger="click" class="more-dropdown" style="line-height: 35px;">
<el-button size="mini" type="primary">缩 放</el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item><span class="el-button--text-" @click="scale = 1">1X</span></el-dropdown-item>
<el-dropdown-item><span class="el-button--text-" @click="scale = 2">2X</span></el-dropdown-item>
<el-dropdown-item><span class="el-button--text-" @click="scale = 3">3X</span></el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</div>
<!-- PDF容器 -->
<div ref="pdfContainer" class="pdf-container" @scroll="handleScroll">
<!-- 占位符,用于撑开滚动区域 -->
<div :style="{ height: totalHeight + 'px' }"></div>
<!-- 渲染可见页面 -->
<div v-for="page in visiblePages" :key="page.pageNumber" :ref="`page-${page.pageNumber}`" class="page-container"
:style="{ top: getPageTop(page.pageNumber) + 'px' }">
<canvas :ref="`canvas-${page.pageNumber}`"></canvas>
<div v-if="page.loading" class="loading-indicator">加载中...</div>
</div>
</div>
</div>
</template>
<script>
import pdfjsLib from 'pdfjs-dist/build/pdf';
import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.entry';
import { debounce } from 'lodash';
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker;
export default {
data() {
return {
pdfDoc: null,
loading: true,
totalPages: 0,
pageHeights: [],
totalHeight: 0,
visiblePages: [],
scale: 1,
baseScale: 1, // 移动端基准缩放比例
scrollTop: 0,
viewport: null,
renderTasks: {},
loadedPages: {}, // 新增:记录已加载的页面
desiredTotal: 5, // 每次加载的页面数量
canvasPool: []
};
},
mounted() {
const pageSize = this.$route.query.pageSize
this.desiredTotal = pageSize?pageSize:this.desiredTotal
console.log('预加载页面数量: -->', this.desiredTotal);
this.loadPdf(this.pdfUrl);
this.debouncedHandleScroll = debounce(this.updateVisiblePages, 100);
this.debouncedHandleResize = debounce(() => {
this.calculatePageHeights();
this.updateVisiblePages();
}, 100);
window.addEventListener('resize', this.handleResize);
},
beforeDestroy() {
window.removeEventListener('resize', this.handleResize);
this.debouncedHandleScroll.cancel();
this.debouncedHandleResize.cancel();
Object.values(this.renderTasks).forEach((task) => task.cancel());
},
computed: {
pdfUrl() {
return this.$route.query.pdfUrl;
},
},
watch: {
scale(newVal) {
this.loadedPages = {}
this.updateVisiblePages()
}
},
methods: {
switchViewMode() {
this.$router.replace({
name: 'pagePreviewPdf',
query: {
pdfUrl: this.pdfUrl
}
})
},
onClose() {
this.$router.go(-1)
},
// 放大
scalBig() {
this.scale = this.scale + 0.2
},
// 缩小
scalSmall() {
if (this.scale > 1.2) {
this.scale = this.scale - 0.2
}
},
async loadPdf(url) {
try {
const loadingTask = pdfjsLib.getDocument(url);
this.pdfDoc = await loadingTask.promise;
this.totalPages = this.pdfDoc.numPages;
await this.calculatePageHeights();
this.updateVisiblePages();
this.loading = false;
} catch (error) {
console.error('加载PDF失败:', error);
this.loading = false;
this.$message.error('加载PDF失败:' + error);
}
},
handleScroll() {
this.debouncedHandleScroll();
},
handleResize() {
this.debouncedHandleResize();
},
async calculatePageHeights() {
this.pageHeights = [];
let page = await this.pdfDoc.getPage(1);
let viewport = page.getViewport({ scale: 1 });
this.baseScale = this.isMobile ? window.innerWidth / viewport.width : 1
for (let i = 1; i <= this.totalPages; i++) {
const page = await this.pdfDoc.getPage(i);
const viewport = page.getViewport({ scale: this.scale*this.baseScale });
this.pageHeights.push(viewport.height);
console.log(i, ' 页面高度:', viewport.height, 'px,页面宽度:', viewport.width, 'px')
}
this.totalHeight = this.pageHeights.reduce((sum, height) => sum + height, 0);
},
getPageTop(pageNumber) {
const top = this.pageHeights.slice(0, pageNumber - 1).reduce((sum, height) => sum + height, 0)
// console.log('当前显示页面top:', top)
return top;
},
updateVisiblePages() {
const container = this.$refs.pdfContainer;
const scrollTop = container.scrollTop;
const containerHeight = container.clientHeight;
// 计算初始可见范围
let startPage = 1;
let endPage = this.totalPages;
let currentHeight = 0;
// 计算起始页
for (let i = 0; i < this.pageHeights.length; i++) {
currentHeight += this.pageHeights[i];
if (currentHeight > scrollTop) {
startPage = i + 1;
break;
}
}
// 计算结束页
currentHeight = 0;
for (let i = 0; i < this.pageHeights.length; i++) {
currentHeight += this.pageHeights[i];
if (currentHeight > scrollTop + containerHeight) {
endPage = i + 1;
break;
}
}
// 扩展预加载范围,总页数不超过10
const desiredTotal = this.desiredTotal;
const visibleCount = endPage - startPage + 1;
let remaining = desiredTotal - visibleCount;
if (remaining > 0) {
let preloadBefore = Math.floor(remaining / 2);
let preloadAfter = remaining - preloadBefore;
let newStart = Math.max(1, startPage - preloadBefore);
let newEnd = Math.min(this.totalPages, endPage + preloadAfter);
// 边界调整
const addedBefore = startPage - newStart;
const addedAfter = newEnd - endPage;
if (addedBefore < preloadBefore) {
newEnd = Math.min(this.totalPages, newEnd + (preloadBefore - addedBefore));
} else if (addedAfter < preloadAfter) {
newStart = Math.max(1, newStart - (preloadAfter - addedAfter));
}
startPage = newStart;
endPage = newEnd;
// 确保不超过总页数限制
if (endPage - startPage + 1 > desiredTotal) {
endPage = startPage + desiredTotal - 1;
if (endPage > this.totalPages) endPage = this.totalPages;
}
} else {
// 可见页数超过10时调整
endPage = startPage + desiredTotal - 1;
if (endPage > this.totalPages) {
endPage = this.totalPages;
startPage = Math.max(1, endPage - desiredTotal + 1);
}
}
// 生成可见页面数组
this.visiblePages = [];
console.log('渲染显示范围:', startPage, ' --- ', endPage)
const pages = []
for (let j = startPage; j <= endPage; j++) {
pages.push(j + '')
}
console.log('>>>>loadedPages:', JSON.stringify(this.loadedPages))
// 移除不在显示区的页面
Object.keys(this.loadedPages).filter(k => !pages.includes(k)).forEach(pageNumber => {
this.$delete(this.loadedPages, pageNumber);
})
console.log('>>>>cleaned loadedPages:', JSON.stringify(this.loadedPages))
for (let i = startPage; i <= endPage; i++) {
const isLoaded = !!this.loadedPages[i];
this.visiblePages.push({
pageNumber: i,
loading: !isLoaded,
});
if (!isLoaded) {
this.renderPage(i);
console.log('渲染', i)
} else {
console.log('DONE:', i)
}
}
},
async renderPage(pageNumber) {
console.log('>>>>loadedPages:', JSON.stringify(this.loadedPages))
if (this.loadedPages[pageNumber]) {
const index = this.visiblePages.findIndex((p) => p.pageNumber === pageNumber);
if (index !== -1) this.$set(this.visiblePages[index], 'loading', false);
return;
}
if (this.renderTasks[pageNumber]) this.renderTasks[pageNumber].cancel();
try {
const page = await this.pdfDoc.getPage(pageNumber);
const canvas = this.$refs[`canvas-${pageNumber}`][0];
const context = canvas.getContext('2d');
const viewport = page.getViewport({ scale: this.scale*this.baseScale });
canvas.height = viewport.height;
canvas.width = viewport.width;
const renderTask = page.render({ canvasContext: context, viewport });
this.renderTasks[pageNumber] = renderTask;
await renderTask.promise;
this.$set(this.loadedPages, pageNumber, true); // 标记为已加载
const index = this.visiblePages.findIndex((p) => p.pageNumber === pageNumber);
if (index !== -1) this.$set(this.visiblePages[index], 'loading', false);
} catch (error) {
if (error.name !== 'RenderingCancelledException') console.error('渲染失败:', error);
}
},
},
};
</script>
<style lang="scss" scoped>
.pdf-container {
height: 100vh;
overflow-y: auto;
width: 100%;
position: relative;
}
.page-container {
position: absolute;
width: 100%;
background: #f5f5f5;
margin-bottom: 20px;
display: flex;
justify-content: center;
}
.loading-indicator {
text-align: center;
padding: 20px;
}
.fixed-toolbar {
position: fixed;
bottom: 50%;
right: 0;
background-color: white;
/* 可选:设置背景颜色 */
opacity: 0.7;
z-index: 1000;
/* 确保工具栏在其他内容之上 */
padding: 10px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
/* 可选:添加阴影效果 */
margin-bottom: 10px;
flex-wrap: wrap;
}
.mb4 {
margin-bottom: 4px;
}
@media screen and (max-width: 768px) {}
</style>