uni-app 中封装 Canvas PDF 阅读器及盖章功能
实现原理
使用 uni-app 的 <web-view>
嵌入本地 HTML 页面,通过 PDF.js 来渲染 PDF,实现跨平台的移动端 PDF 浏览。PDF.js 是一个纯 JavaScript 库,基于 HTML5 Canvas API 绘制 PDF 页面。在实现上,本地 HTML 页面包含一个用于显示 PDF 的容器(如 <div id="myPDF">
),底层通过 Canvas 渲染每一页内容,并支持滑动翻页、缩放等交互。盖章盖章功能则通过在 PDF 预览层上叠加可拖拽的 DOM 图层来实现:页面上创建一个绝对定位的容器(如 #my-signImg
)用来显示盖章图片,用户可通过触摸事件拖动或通过右下角的缩放柄调整大小。当前时间戳则先在一个隐藏的 Canvas 上绘制,然后转成图片添加到叠加层。所有交互产生的数据(如盖章位置、页码等)通过 uni.postMessage
发送回 uni-app 侧进行后续处理。PDF.js 自身对多页有良好支持,可渲染多页并内置缩略图、缩放、翻页等功能,因此该实现可以在单一容器中连续查看多页 PDF 并对任意一页进行签章。
关键模块
PDF 预览模块:位于
SignPdf.vue
,使用<web-view :src="...">
加载hybrid/html/readPdf/index/index.html
。该页面通过引入pdf.js
、jquery.touchPDF.js
等脚本,完成 PDF 文档的加载和渲染。用户在此视图中可以翻页、缩放 PDF 内容。签章叠加层:在 HTML 中以一个绝对定位的
div
作为盖章容器(#my-signImg
),内部含有盖章图片(<img id="signUrl">
)和缩放柄(<span class="br">
)。盖章完成后,还会动态创建另一个类似的容器用于显示时间戳图片。通过监听触摸事件,实现对这两个层的拖拽移动和右下角拉伸缩放。盖章画板模块:独立的
sign.vue
页面提供一个可手写盖章的画布(<canvas>
),用户可以绘制或清除笔迹、将盖章保存成图片等。完成后可返回上一页(SignPdf)以作为盖章图像。通信模块:Web 页面的盖章操作(点击“重新盖章”或“保存”)会调用
uni.postMessage()
向 uni-app 页面传递指令或数据(如{type:'reSign'}
或签章位置信息)。SignPdf.vue
的@message
事件监听接收这些数据,根据类型跳转到盖章页面或结束签章流程。
技术亮点
纯前端跨平台渲染:采用 PDF.js 在浏览器/WebView 中渲染 PDF,无需依赖原生组件或第三方插件,可统一 iOS、Android 环境,开发和调试效率高。基于 Canvas 的绘制保证了在不同设备上对文字和图形的渲染一致性。
交互友好:结合
jquery.touchPDF.js
、jquery.panzoom.js
等插件,实现了手势缩放、滑动翻页等移动端友好功能;盖章盖章通过可视化拖拽、拉伸操作完成,符合用户习惯。多页文档支持:PDF.js 天生支持多页文档的加载与渲染,该 Demo 可连续翻阅多页并针对任意页添加盖章盖章;同时提供缩略图、缩放控制等功能。
部分代码
<template>
<view class="content">
<view>
<web-view :src="websrc" @message="handleMessage"></web-view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
websrc: '',
appUrl: '/hybrid/html/readPdf/index/index.html', //app内的web地址
pdfUrl: {
url: './my.pdf'
},
signImgUrl: {
url: './index (1).png'
},
title: '合同盖章', //pdf文件名称
}
},
onLoad(query) {
this.refresh(query)
},
methods: {
async refresh(query) {
try {
uni.showLoading({
title: '加载中'
})
//app 直接跳转到app内的web页面
this.websrc = this.appUrl + '?url=' + encodeURIComponent(this.pdfUrl.url) + '&tname=' + encodeURIComponent(this.title) + '&signImgUrl=' + encodeURIComponent(this.signImgUrl.url)
} catch (e) {
uni.showModal({
title: '发生错误',
content: e
})
} finally {
uni.hideLoading()
}
},
async handleMessage(evt) {
if (evt.detail.data[0]?.type == 'message') {
uni.showToast({
title: evt.detail.data[0]?.content,
duration: 2000
});
}else if (evt.detail.data[0]?.type == 'loading') {
uni.showLoading({
title: '导出pdf中...'
});
}else if (evt.detail.data[0]?.type == 'save') {
uni.showLoading({
title: '正在打开...'
});
plus.io.requestFileSystem(plus.io.PRIVATE_DOC, function(fs) {
console.log(fs.root.fullPath)
const filePath = `${fs.root.fullPath}files/${Date.now()}.pdf`;
console.log(`filePath: ${filePath}`); // 打印正确的filePath
fs.root.getFile(filePath, {
create: true
}, function(fileEntry) {
console.log('filePath' + filePath)
fileEntry.createWriter(function(writer) {
writer.onwrite = function() {
console.log(`success filePath: ${filePath}`); // 在输出时使用模板字符串
uni.openDocument({
filePath: filePath,
showMenu: true,
success: () => {
uni.hideLoading()
},
fail: (error) => {
uni.hideLoading()
console.log(error.message || '操作失败,请稍后再试');
}
})
}
writer.onerror = function(e) {
uni.hideLoading()
console.log('写入文件失败:' + e.message)
}
// console.log(evt.detail.data[0]?.base64);
writer.writeAsBinary(evt.detail.data[0]?.base64)
})
})
},
}
}
},
}
</script>
<style>
</style>