功能简述:在Drawer组件里的分步表单中,根据上一步上传的pdf文件,加载pdf文件,拖拽元素到pdf,拖拽完以后使用canvas画一个占位符(标记章的位置),并把元素坐标位置信息上传到上上签,在指定的位置生成印章和签名,使用的ui库是view-design,具体实现步骤如下
- 安装并引入第三方库pdfjs-dist
import * as pdfjsLib from 'pdfjs-dist';
- 加载PDF文件,这里使用了两个canvas,一个用于渲染pdf,一个用于在pdf上面蒙上一个画布,把拖拽元素生成的占位符在画布上显示。避免混在一起,造成一些渲染白屏的问题。
renderPDF() {
// 加载PDF
pdfjsLib.getDocument(this.getUploadPdfUrl).promise.then(pdf => {
this.pdf = pdf
this.totalPages = pdf.numPages
// 初始化渲染第一页
this.renderPage(this.currentPage)
})
},
async renderPage(pageNumber) {
if (pageNumber > this.totalPages) {
return
}
const pdfPage = await this.pdf.getPage(pageNumber)
const viewport = pdfPage.getViewport({ scale: 1.5 })
// 创建pdf-canvas
const pdfCanvas = document.createElement('canvas')
const canvasContext = pdfCanvas.getContext('2d')
// 创建draw-canvas
const drawCanvas = document.createElement('canvas')
// const canvasContext = drawCanvas.getContext('2d')
pdfCanvas.width = drawCanvas.width = viewport.width
pdfCanvas.height = drawCanvas.height = viewport.height
this.pdf.getPage(pageNumber).then(page => {
const renderTask = page.render({
canvasContext,
viewport,
})
renderTask.promise.then(() => {
// 渲染完成后销毁旧的Canvas元素
if (this.$refs.pdfCanvas) {
this.$refs.pdfCanvas.parentNode.removeChild(this.$refs.pdfCanvas)
}
if (this.$refs.drawCanvas) {
this.$refs.drawCanvas.parentNode.removeChild(this.$refs.drawCanvas)
}
// 将新的Canvas元素添加到DOM中
this.$refs.scrollContainer.appendChild(pdfCanvas)
this.$refs.scrollContainer.appendChild(drawCanvas)
this.$refs.pdfCanvas = pdfCanvas
this.$refs.drawCanvas = drawCanvas
this.$refs.pdfCanvas.style = `position: absolute
this.$refs.drawCanvas.style = `position: absolute
this.currentPage = pageNumber
this.drawRectangles()
})
})
},
- 调用drawRectangles()方法,绘制占位符,把每页pdf上面的拖拽元素坐标信息保存在rect对象中,用于后续的编辑时回显盖章位置坐标
async drawRectangles() {
const canvas = this.$refs.drawCanvas
const context = canvas.getContext('2d')
// 清空画布
context.clearRect(0, 0, canvas.width, canvas.height)
this.rect[this.currentPage]?.forEach(rectangle => {
let width = 0
if (rectangle.type === 'seal' && rectangle.signType === 1) {
// 签名
context.fillStyle = 'yellow'
context.fillRect(rectangle.x, rectangle.y, 134, 60)
context.fillStyle = '#000'
context.font = '14px Arial'
context.textAlign = 'center'
context.textBaseline = 'middle'
const text = '签名处'
context.fillText(text, rectangle.x + 67, rectangle.y + 30)
width = 134
} else if (rectangle.type === 'seal' && rectangle.signType === 0) {
// 签章
context.fillStyle = 'red'
context.fillRect(rectangle.x, rectangle.y, 200, 200)
context.fillStyle = '#000'
context.font = '14px Arial'
context.textAlign = 'center'
context.textBaseline = 'middle'
const text = '盖章处'
context.fillText(text, rectangle.x + 100, rectangle.y + 100)
width = 200
} else if (rectangle.type === 'date' && rectangle.signType === 1) {
context.fillStyle = 'yellow'
context.fillRect(rectangle.x, rectangle.y, 140, 40)
context.fillStyle = '#000'
context.font = '14px Arial'
context.textAlign = 'center'
context.textBaseline = 'middle'
const text = '签署日期'
context.fillText(text, rectangle.x + 70, rectangle.y + 20)
width = 140
} else {
context.fillStyle = 'red'
context.fillRect(rectangle.x, rectangle.y, 140, 40)
context.fillStyle = '#000'
context.font = '14px Arial'
context.textAlign = 'center'
context.textBaseline = 'middle'
const text = '签署日期'
context.fillText(text, rectangle.x + 70, rectangle.y + 20)
width = 140
}
// 绘制圆
const centerX = rectangle.x + width
const centerY = rectangle.y
const radius = 10
context.beginPath()
context.arc(centerX, centerY, radius, 0, 2 * Math.PI)
context.fillStyle = 'rgba(0, 0, 0, 0.5)'
context.fill()
// 绘制删除按钮
context.fillStyle = 'white'
context.font = '14px Arial'
context.textAlign = 'center'
context.textBaseline = 'middle'
context.fillText('X', centerX, centerY)
})
// 绘制矩形占位符
canvas.addEventListener('click', this.onClickCanvas)
},
- 拖拽的占位符点击右上角删除按钮,支持删除对应的占位符
onClickCanvas(event) {
const canvas = this.$refs.drawCanvas
const canvasRect = canvas.getBoundingClientRect()
const clickX = event.clientX - canvasRect.left
const clickY = event.clientY - canvasRect.top
// 检查是否点击了删除按钮
this.rect[this.currentPage]?.forEach((rectangle, index) => {
let width = 0
if (rectangle.type === 'seal' && rectangle.signType === 1) {
// 签名
width = 134
} else if (rectangle.type === 'seal' && rectangle.signType === 0) {
// 签章
width = 200
} else {
// 日期
width = 140
}
const deleteButtonX = rectangle.x + width - 10
const deleteButtonY = rectangle.y - 10
if (
clickX >= deleteButtonX &&
clickX <= deleteButtonX + 20 &&
clickY >= deleteButtonY &&
clickY <= deleteButtonY + 20
) {
this.rect[this.currentPage].splice(index, 1)
this.drawRectangles()
}
})
},
- 处理scrollContainer的拖拽事件,拖拽完成时计算出拖拽元素相对pdf的位置坐标,并根据坐标位置添加相应的占位符,在拖拽的过程中,还会判断越界情况
handleDragover(event) {
event.preventDefault();
},
handleDrop(event) {
event.preventDefault();
const canvas = this.$refs.drawCanvas;
const rect = canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
this.addRectanglePlaceholder(x, y);
},
addRectanglePlaceholder(x, y) {
const canvas = this.$refs.drawCanvas;
const percentX = ((x - this.offsetX) / canvas.width).toFixed(2);
const percentY = Math.abs((y - this.offsetY) / canvas.height).toFixed(2);
const rectangle = {
x: x - this.offsetX,
y: y - this.offsetY,
signId: this.signId,
type: this.dragType,
signType: this.signType,
pageNum: this.currentPage,
percentX,
percentY,
};
if (this.dragType === 'seal' && this.signType === 1) {
if (x - this.offsetX + 134 >= canvas.width || x - this.offsetX < 0) {
this.$Message.error({
content: '拖动位置已超出合同边界,请重新拖动',
duration: 5,
});
return;
}
}
if (this.dragType === 'seal' && this.signType === 0) {
if (x - this.offsetX + 200 >= canvas.width || x - this.offsetX < 0) {
this.$Message.error({
content: '拖动位置已超出合同边界,请重新拖动',
duration: 5,
});
return;
}
}
if (this.dragType === 'date') {
if (x - this.offsetX + 140 >= canvas.width || x - this.offsetX < 0) {
this.$Message.error({
content: '拖动位置已超出合同边界,请重新拖动',
duration: 5,
});
return;
}
}
if (!this.rect[this.currentPage]) {
this.rect[this.currentPage] = [];
this.rect[this.currentPage].push(rectangle);
} else {
this.rect[this.currentPage].push(rectangle);
}
this.drawRectangles();
},
handleDragStart(event) {
this.offsetX = event.offsetX;
this.offsetY = event.offsetY;
const itemElement = event.target;
const type = itemElement.getAttribute('data-type');
this.dragType = type;
},
- 编辑进来回显盖章和签名的坐标位置信息
process() {
const canvas = this.$refs.drawCanvas;
const resultMap = {};
this.sealCoordinateList?.forEach(it => {
if (it.sealPageNum) {
if (resultMap[it.sealPageNum]) {
resultMap[it.sealPageNum].push({
x: Math.floor(it.sealX * canvas.width),
y: Math.floor(it.sealY * canvas.height),
signId: it.signId,
signType: it.signType,
type: 'seal',
pageNum: it.sealPageNum,
percentX: it.sealX,
percentY: it.sealY,
});
} else {
resultMap[it.sealPageNum] = [ { x: Math.floor(it.sealX * canvas.width), y: Math.floor(it.sealY * canvas.height), signId: it.signId, signType: it.signType, type: 'seal', pageNum: it.sealPageNum, percentX: it.sealX, percentY: it.sealY, }, ];
}
}
if (it.datePageNum) {
if (resultMap[it.datePageNum]) {
resultMap[it.datePageNum].push({
x: Math.floor(it.dateX * canvas.width),
y: Math.floor(it.dateY * canvas.height),
signId: it.signId,
signType: it.signType,
type: 'date',
pageNum: it.datePageNum,
percentX: it.dateX,
percentY: it.dateY,
});
} else {
resultMap[it.datePageNum] = [ { x: Math.floor(it.dateX * canvas.width), y: Math.floor(it.dateY * canvas.height), signId: it.signId, signType: it.signType, type: 'date', pageNum: it.datePageNum, percentX: it.dateX, percentY: it.dateY, }, ];
}
}
});
this.rect = resultMap;
},
- template模板内容
<template>
<div class="sign-position-box">
<div class="drag-container">
<div class="fixed">
<div class="first-step">
<div class="text">
第一步:选择签约方
</div>
<RadioGroup v-model="sign" vertical @on-change="handleRadioChange">
<Radio v-for="item in getSignaryInfo" :key="item.name" :label="item.name">
{{ item.name }}
</Radio>
</RadioGroup>
</div>
<div class="second-step">
<div class="text">
第二步:拖动签约位置
</div>
<div class="sign-box">
<div
v-if="signType === 1"
class="seal"
draggable="true"
data-type="seal"
@dragstart="handleDragStart"
>
<div class="circle">
签
</div>
<span>签名</span>
</div>
<div
v-else
class="seal"
draggable="true"
data-type="seal"
@dragstart="handleDragStart"
>
<div class="circle">
章
</div>
<span>盖章</span>
</div>
<div
class="seal"
draggable="true"
data-type="date"
@dragstart="handleDragStart"
>
<div class="circle">
日
</div>
<span>签署日期</span>
</div>
</div>
</div>
</div>
</div>
<!-- <div id="fileContent" class="file-content" /> -->
<div ref="scrollContainer" class="file-content">
<canvas ref="pdfCanvas" class="pdf-canvas" />
<canvas ref="drawCanvas" class="draw-canvas" />
</div>
</div>
</template>