注意
使用的是pdf.js 版本为 v2.16.105。因为新版本 兼容性不太好,部分手机预览不了,所以采用v2版本。
相关依赖
javascript
"canvas": "^2.11.2",
"pdfjs-dist": "^2.16.105",
"core-js-pure": "^3.37.1",
"hammerjs": "^2.0.8", //这个是写手势 双指缩放的 不需要可以去掉
解决部分浏览器或者手机系统的兼容问题
javascript
//解决 structuredClone
// https://developer.mozilla.org/en-US/docs/Web/API/structuredClone#browser_compatibility
// https://gitcode.com/zloirock/core-js/overview?utm_source=csdn_github_accelerator
import structuredClone from 'core-js-pure/actual/structured-clone';
// 解决 TypeError: key.split(...).at is not a function
// https://github.com/wojtekmaj/react-pdf/issues/1465
import 'core-js/features/array/at';
window.structuredClone = structuredClone;
代码
以下为在uniapp vue3 实现 h5 预览pdf文件的代码 有使用vant(手指缩放功能只写了一点,是不能用的)。
javascript
<template>
<div id="pdf-view" ref="pdfView">
<!-- <canvas v-for="page in state.pdfPages" :key="page" id="pdfCanvas" />-->
<div ref="pdfViewContainer">
<div
v-for="pageNumber in state.pdfPages"
v-show="state.pdfPageList.includes(pageNumber)"
:key="pageNumber"
:ref="(el) => (pageRefs[pageNumber - 1] = el)"
></div>
</div>
<je-loading v-show="loading" />
</div>
</template>
<script setup>
//解决 structuredClone
// https://developer.mozilla.org/en-US/docs/Web/API/structuredClone#browser_compatibility
// https://gitcode.com/zloirock/core-js/overview?utm_source=csdn_github_accelerator
import structuredClone from 'core-js-pure/actual/structured-clone';
// 解决 TypeError: key.split(...).at is not a function
// https://github.com/wojtekmaj/react-pdf/issues/1465
import 'core-js/features/array/at';
import * as pdfjsWorker from 'pdfjs-dist/lib/pdf.worker.js';
// 解决 pdfjsWorker 未定义
window.pdfjsWorker = pdfjsWorker;
window.structuredClone = structuredClone;
// if (!Array.prototype.at) {
// Array.prototype.at = function (index) {
// if (index < 0) {
// index = this.length + index;
// }
// if (index >= 0 && index < this.length) {
// return this[index];
// }
// return undefined;
// };
// }
import Hammer from 'hammerjs';
import * as pdfjsWorker from 'pdfjs-dist/lib/pdf.worker.js';
// 解决 pdfjsWorker 未定义
window.pdfjsWorker = pdfjsWorker;
import 'pdfjs-dist/web/pdf_viewer.css';
import * as PDF from 'pdfjs-dist';
// import * as PDF from 'pdfjs-dist/build/pdf.js';
import { useRoute } from 'vue-router';
import { ref, reactive, onMounted, nextTick, defineProps } from 'vue';
import { showFailToast } from 'vant';
const route = useRoute();
const props = defineProps({
src: {
type: String,
default: '',
},
});
const pdfViewContainer = ref(null);
const pdfView = ref(null);
const pageRefs = ref([]);
const loading = ref(false);
const state = reactive({
// 总页数
pdfPages: 1,
pdfPageList: [], //有效页码列表
// 页面缩放
pdfScale: 1,
});
let pdfDoc = null;
async function loadFile(url) {
// {
// url,
// cMapUrl: 'https://cdn.jsdelivr.net/npm/pdfjs-dist@2.16.105/cmaps/',
// cMapPacked: true,
// }
loading.value = true;
// 设置配置选项 手势缩放
PDF?.DefaultViewerConfig?.set({
handToolOnDblClick: true,
mouseWheelScale: true,
});
let arrayBufferPDF;
//
// if (navigator.userAgent.indexOf('QQ')) {
// const pdfData = await fetch(url);
// arrayBufferPDF = await pdfData.arrayBuffer();
// }
// 解决部分机型浏览器 undefined is not an object(evaluating 'response.body.getReader')
// https://www.qingcong.tech/technology/javascript/a-pdfjs-bug-in-qq.html#%E8%A7%A3%E5%86%B3%E5%8A%9E%E6%B3%95
fetch(url).then(async (pdfData) => {
console.log('pdfData', pdfData);
if (!pdfData.ok) {
loading.value = false;
showFailToast({
message: '预览地址不存在或已失效',
duration: 0,
});
// window.JE.alert('预览地址不存在', 'error');
return;
}
arrayBufferPDF = await pdfData.arrayBuffer();
const loadingTask = arrayBufferPDF
? PDF.getDocument({ data: arrayBufferPDF })
: PDF.getDocument(url);
loadingTask.promise.then((pdf) => {
pdfDoc = pdf;
// 获取pdf文件总页数
state.pdfPages = pdf.numPages;
nextTick(() => {
for (let i = 0; i < state.pdfPages; i++) {
renderPage(i + 1); // 从第一页开始渲染
}
});
});
});
}
function initPinchZoom() {
const pdfViewEl = pdfView.value;
const hammer = new Hammer(pdfViewEl);
// 启用捏合缩放手势
hammer.get('pinch').set({ enable: true });
// 启用拖动手势,设置拖动方向为所有方向,阈值为0
hammer.get('pan').set({ direction: Hammer.DIRECTION_ALL, threshold: 0 });
let initialScale = 1; // 初始缩放比例
let deltaX = 0; // 当前水平拖动距离
let deltaY = 0; // 当前垂直拖动距离
let startX = 0; // 拖动开始时的水平位置
let startY = 0; // 拖动开始时的垂直位置
const MIN_SCALE = 1; // 最小缩放比例
const MAX_SCALE = 4; // 最大缩放比例
let lastPinchTime = 0; // 上一次捏合事件的时间戳
let lastPanTime = 0; // 上一次拖动事件的时间戳
// 捏合开始事件处理函数
hammer.on('pinchstart', (event) => {
initialScale = state.pdfScale; // 记录初始缩放比例
startX = deltaX; // 记录拖动开始时的水平位置
startY = deltaY; // 记录拖动开始时的垂直位置
});
// 捏合移动事件处理函数
hammer.on('pinchmove', (event) => {
const currentTime = Date.now();
// 节流控制,限制事件触发频率
if (currentTime - lastPinchTime > 50) {
event.preventDefault();
const scale = event.scale; // 获取当前捏合的缩放比例
const newScale = Math.min(Math.max(initialScale * scale, MIN_SCALE), MAX_SCALE); // 计算新的缩放比例,限制在最小和最大缩放比例之间
state.pdfScale = newScale; // 更新缩放比例状态
applyTransform(); // 应用变换
lastPinchTime = currentTime; // 更新上一次捏合事件的时间戳
}
});
// 捏合结束事件处理函数
hammer.on('pinchend', (event) => {
initialScale = state.pdfScale; // 更新初始缩放比例为当前缩放比例
limitPanPosition(); // 限制拖动位置范围
renderPages(); // 重新渲染页面
});
// 拖动开始事件处理函数
hammer.on('panstart', (event) => {
pdfViewEl.style.transition = 'none'; // 禁用拖动过渡效果
startX = deltaX; // 记录拖动开始时的水平位置
startY = deltaY; // 记录拖动开始时的垂直位置
});
// 拖动移动事件处理函数
hammer.on('panmove', (event) => {
const currentTime = Date.now();
// 节流控制,限制事件触发频率
if (currentTime - lastPanTime > 50) {
const dx = event.deltaX; // 获取当前拖动的水平距离
const dy = event.deltaY; // 获取当前拖动的垂直距离
deltaX = startX + dx; // 计算新的水平拖动距离
deltaY = startY + dy; // 计算新的垂直拖动距离
applyTransform(); // 应用变换
lastPanTime = currentTime; // 更新上一次拖动事件的时间戳
}
});
// 拖动结束事件处理函数
hammer.on('panend', (event) => {
pdfViewEl.style.transition = 'transform 0.3s ease'; // 启用拖动过渡效果
limitPanPosition(); // 限制拖动位置范围
});
// 限制拖动位置范围的函数
function limitPanPosition() {
const pdfWidth = pdfViewEl.clientWidth * state.pdfScale; // 计算PDF页面的实际宽度
const containerWidth = pdfViewContainer.value.clientWidth; // 获取容器的宽度
const containerHeight = pdfViewContainer.value.clientHeight; // 获取容器的高度
// 计算单个页面的平均高度
const averagePageHeight =
pageRefs.value.reduce((totalHeight, pageRef) => {
return totalHeight + (pageRef ? pageRef.clientHeight : 0);
}, 0) / state.pdfPageList.length;
// 估算总高度,使用PDF文档的总页数乘以单个页面的平均高度
const estimatedTotalHeight = state.pdfPages * averagePageHeight * state.pdfScale;
// 限制水平拖动距离,确保PDF页面在容器内部
deltaX = Math.min(0, Math.max(deltaX, containerWidth - pdfWidth));
// 限制垂直拖动距离,确保PDF页面在容器内部,使用估算的总高度
deltaY = Math.min(0, Math.max(deltaY, containerHeight - estimatedTotalHeight));
applyTransform(); // 应用变换
}
// 应用变换的函数
function applyTransform() {
pdfViewEl.style.transform = `translate(${deltaX}px, ${deltaY}px) scale(${state.pdfScale})`; // 设置PDF页面的变换样式
}
}
function renderPages() {
state.pdfPageList = [];
for (let i = 0; i < state.pdfPages; i++) {
renderPage(i + 1);
}
}
function renderPage(num) {
pdfDoc.getPage(num).then((page) => {
// 获取当前页面对应的DOM容器元素
const container = pageRefs.value[num - 1];
// 创建一个新的canvas元素
const canvas = document.createElement('canvas');
// 获取canvas的2D渲染上下文
const ctx = canvas.getContext('2d');
// 获取设备像素比
let devicePixelRatio = window.devicePixelRatio || 1;
// 获取画布的backing store ratio
let backingStoreRatio =
ctx.webkitBackingStorePixelRatio ||
ctx.mozBackingStorePixelRatio ||
ctx.msBackingStorePixelRatio ||
ctx.oBackingStorePixelRatio ||
ctx.backingStorePixelRatio ||
1;
// 获取pdfViewContainer元素的宽度
const pdfWrapperElWidth =
pdfViewContainer.value.clientWidth ||
pdfViewContainer.value.offsetWidth ||
pdfViewContainer.value.style.width;
// 获取PDF页面的初始视口,缩放比例为1
const intialisedViewport = page.getViewport({ scale: 1 });
// 计算缩放比例,使PDF页面宽度与容器宽度一致
const scale = pdfWrapperElWidth / intialisedViewport.width;
// 计算设备像素比与backing store ratio的比值
let ratio = devicePixelRatio / backingStoreRatio;
// 根据缩放比例获取PDF页面的视口
const viewport = page.getViewport({ scale });
// 设置canvas的宽度为容器宽度乘以ratio,确保高分辨率下的清晰度
canvas.width = pdfWrapperElWidth * ratio;
// 设置canvas的高度为视口高度乘以ratio,确保高分辨率下的清晰度
canvas.height = viewport.height * ratio;
// 设置canvas的样式宽度为100%,与容器宽度一致
canvas.style.width = '100%';
// 设置canvas的样式高度为auto,根据宽度自适应
canvas.style.height = 'auto';
// 缩放画布的渲染上下文,根据ratio进行缩放,确保在高分辨率下绘制的清晰度
ctx.scale(ratio, ratio);
const renderContext = {
canvasContext: ctx,
viewport,
};
// 设置页面容器的高度为视口高度
container.style.height = `${viewport.height}px`;
page
.render(renderContext)
.promise.then(() => {
state.pdfPageList.push(num);
// 如果 container 存在 canvas元素 覆盖canvas元素
container?.firstChild && container.removeChild(container.firstChild);
container && container.appendChild(canvas);
})
.finally(() => {
if (num === state.pdfPages) {
loading.value = false;
}
});
});
}
onMounted(() => {
const file = route.query.file && JSON.parse(decodeURIComponent(route.query.file));
const { relName, previewUrl } = file || {};
if (relName) {
// 设置 uniapp 当前页面标题
uni.setNavigationBarTitle({
title: relName,
});
}
if (previewUrl) {
loadFile(previewUrl);
// nextTick(() => {
// initPinchZoom();
// });
} else {
showFailToast({
message: '预览地址不存在',
duration: 0,
});
}
});
</script>
<style scoped lang="less">
uni-page-body {
overflow-y: scroll;
}
</style>