uniapp使用 movable-area movable-view 实现按双指中心位置缩放及拖拽功能
<template>
<div style="width: 100%; height: 100%; position: relative; overflow-y: auto">
<movable-area id="pdf-view" ref="pdfView" class="content-box" :scale-area="true">
<movable-view
:out-of-bounds="true"
:style="{
transformOrigin: `${state.x}px ${state.y}px 0`,
}"
class="movableView"
direction="all"
:scale="true"
scale-min="1"
damping="1"
ref="movableViewRef"
friction="2"
:x="0"
:y="0"
:scale-max="4"
:animation="false"
@scale="pdfScaleChange"
@touchstart="touchStart"
@touchmove="touchMove"
@touchend="touchEnd"
@change="onScroll"
@click="onViewClick"
>
<div
:style="{
width: '15px',
height: '15px',
background: 'gold',
position: 'absolute',
left: state.x + 'px',
top: state.y + 'px',
transform: `translate(-50%,-50%)`,
zIndex: 100,
}"
>
</div>
<div ref="pdfViewContainer">
<div
v-for="pageNumber in state.pdfPages"
:key="pageNumber"
:ref="(el) => (pageRefs[pageNumber - 1] = el)"
@touchstart="onPdfClick($event, pageNumber)"
></div>
</div>
</movable-view>
</movable-area>
</div>
<je-loading v-show="loading" />
</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=****_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;
// 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 * 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';
import { onPageScroll } from '@dcloudio/uni-app';
const route = useRoute();
const props = defineProps({
src: {
type: String,
default: '',
},
});
const pdfViewContainer = ref(null);
const movableViewRef = ref(null);
const pdfView = ref(null);
const pageRefs = ref([]);
const loading = ref(false);
const state = reactive({
// 总页数
pdfPages: 1,
pdfPageList: [], //有效页码列表
// 页面缩放
pdfScale: 1,
x: 0,
y: 0,
translateX: 0,
translateY: 0,
sCenterX: 0,
sCenterY: 0,
startTouches: [], // 初始触摸列表
endTouches: [], // 结束触摸列表
scrollTop: 0,
scrollPage: 1,
currentHeight: 0,
currentWidth: 0,
xData: 0,
});
const cData = reactive({
move: {},
click: {},
});
let pdfDoc = null;
const onPdfClick = (e, page) => {
// state.scrollTop = e.clientY;
state.scrollPage = page;
state.currentHeight = pageRefs.value[state.scrollPage - 1].clientHeight;
state.currentWidth = pageRefs.value[state.scrollPage - 1].clientWidth;
// console.log('height', pageRefs.value[state.scrollPage - 1]);
};
const pdfScaleChange = (e) => {
console.log('放大', e);
state.pdfScale = e.detail.scale;
state.xData = e.detail.x;
state.scrollTop = e.detail.y;
getTranslate();
};
const touchStart = (event) => {
state.startTouches = event.touches;
};
const onViewClick = (event) => {
cData.click = event.touches[0];
};
const touchMove = (event) => {
cData.move = event.touches[0];
state.endTouches = event.touches;
getTranslate();
// 判断是否为双指缩放
if (state.endTouches.length === 2) {
let beforeDistance = calculateCenter(state.startTouches[0], state.startTouches[1]);
// 监听移动,更新中心点坐标
let moveTouch1 = state.startTouches.find((t) => t.identifier === event.touches[0].identifier);
let moveTouch2 = state.startTouches.find((t) => t.identifier === event.touches[1].identifier);
if (moveTouch1 && moveTouch2) {
let centerX = (moveTouch1.clientX + moveTouch2.clientX) / 2;
let centerY = (moveTouch1.clientY + moveTouch2.clientY) / 2;
state.sCenterX = centerX;
state.sCenterY = centerY;
state.y = centerY - state.translateY;
state.x = centerX - state.translateX;
// 边界判断
if (state.x < 0) {
state.x = 0;
} else if (state.x > state.currentWidth) {
state.x = state.currentWidth;
}
let offsetHeight = movableViewRef.value.$el.offsetHeight;
if (state.y < 0) {
state.y = 0;
} else if (state.y > offsetHeight) {
state.y = offsetHeight;
}
}
}
};
const touchEnd = (event) => {
if (event.touches.length < 2) {
state.startTouches = [];
state.endTouches = [];
}
};
const calculateCenter = (startTouches, endTouches) => {
let xDistance = startTouches?.clientX - endTouches?.clientX;
let yDistance = startTouches?.clientY - endTouches?.clientY;
return Math.sqrt(xDistance * xDistance + yDistance * yDistance);
};
onPageScroll((e) => {
// 页面滚动时会触发
// this.scrollTop = e.scrollTop; // 更新滚动位置
// console.log('页面滚动', e.scrollTop);
});
// 获取偏移量
const getTranslate = () => {
let styleM = movableViewRef.value.$el.style;
let transform = styleM.transform;
// 获取y轴偏移量
let y = transform.match(/translateY\(([^)]+)\)/)[1];
// 去除px
y = y.replace('px', '');
// state.translateY = Math.abs(Number(y) || 0);
state.translateY = Number(y);
let x = transform.match(/translateX\(([^)]+)\)/)[1];
x = x.replace('px', '');
// state.translateX = Math.abs(Number(x) || 0);
state.translateX = Number(x);
};
// 设置中心点
const setCenter = () => {
let offsetHeight = movableViewRef.value.$el.offsetHeight;
state.x = state.sCenterX - state.translateX;
state.y = state.sCenterY - state.translateY;
if (state.x < 0) {
state.x = 0;
} else if (state.x > state.currentWidth) {
state.x = state.currentWidth;
}
if (state.y < 0) {
state.y = 0;
} else if (state.y > offsetHeight) {
state.y = offsetHeight;
}
};
const onScroll = (e) => {
// 页面滚动时会触发
state.xData = e.detail.x;
state.scrollTop = e.detail.y;
getTranslate();
setCenter();
};
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 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 || {}