效果就是如下图这样,左边一个签章的名字,拖拽到中间部分的pdf上, 把拖拽的位置存下来点确定的时候把位置和签章的图片信息发给后台。
可参考的网址:https://blog.csdn.net/blue__k/article/details/126723437
引用部分
<dragSign v-if="active==1" ref="dragSign" :signDataList="signDataList" :curfilesListId="curfilesListId" :pdfUrl="pdfUrls" :defaultSign="defaultSign" @submitSign="submitSign" >
<template v-slot:subBtn v-if="showSubmitBtn">
<el-button type="primary" @click="handleSubmit">提交所有签章</el-button>
</template>
</dragSign>
封装的组件
<template>
<el-row :gutter="8">
<!-- 左侧签章可拖拽的按钮部分 -->
<el-col :span="6" style="border:1px solid red">
<div class="sign-img-box">
<div v-for="(item, i) in signDataList" :key="i">
<div class="sign-img labelWidth">{{item.label}}</div>
<div class="btnsWrapper">
<div v-for="(res, index) in item.data" :key="res.id" class="sign-img signBtns">
<el-button :class="stars(res)?'common-button-hightlight':''"
@mousedown="sealPic(res, index)">
<el-icon v-show="stars(res)"><Check /></el-icon>
{{ res.name}}</el-button>
</div>
</div>
</div>
</div>
</el-col>
<!-- 中间展示pdf和拖拽签章图片的部分 -->
<el-col :span="18" style="border:1px solid red">
<div id="signContract" style="display: flex;flex-direction: column; align-items: center; margin-top: 29px;">
<div v-for="page in pages" :key="page" class="pdf-container">
<div>
<div v-show="currentPage === page" :id="'pdf-box' + page" class="pdf-box">
<el-input-number :min="40" :max="150" v-show="sliderShow" class="sliderWrapper" @change="changeSlider" v-model="sliderValue" :step="10"></el-input-number>
<div class="pageTab">
<el-icon class="el-icon-arrow-left" @click="handleUpPage"><ArrowLeftBold /></el-icon>{{currentPage}}/{{pages}}
<el-icon class="el-icon-arrow-left" @click="handleNextPage"><ArrowRightBold/></el-icon>
</div>
<canvas :id="'the-canvas' + page" v-loading="isPdfLoading" />
</div>
</div>
</div>
</div>
</el-col>
</el-row>
<!-- 操作按鈕部分 -->
<el-row style="position:relative">
<div class="my-dialog-footer" >
<el-button @click="upStep">上一步</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
<slot name="subBtn"></slot>
</div>
</el-row>
</template>
<script>
import VuePdfEmbed from "vue-pdf-embed";
import * as PDF from "pdfjs-dist";
// 页面报错解决,在将 pdfjs-dist/build/pdf.worker.js 复制一份放到项目 public 目录下后引入 ---- 避免获取总页数错误的问题
// import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.entry';
// PDFJS.workerSrc = pdfjsWorker;
import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.min'
//PDFJS.GlobalWorkerOptions.workerSrc = pdfjsWorker
// import * as PDFJS from "pdfjs-dist";
// // 页面报错解决,在将 pdfjs-dist/build/pdf.worker.js 复制一份放到项目 public 目录下后引入 ---- 避免获取总页数错误的问题
import * as PDFJS from 'pdfjs-dist/legacy/build/pdf.js' // 注意导入的写法
PDFJS.GlobalWorkerOptions.workerSrc = "pdf.worker.js";
// import { createLoadingTask } from "vue3-pdfjs/esm"; // 获得总页数
import uploadPdf from "../../common/uploadOnePDFAndView";
export default {
name: "dragSign",
components: {uploadPdf,VuePdfEmbed},
computed:{
getPDFurl(){
return this.pdfUrl?this.basicUrl+this.pdfUrl:null
},
stars() {
return (star)=>{
let index=this.signaturePositions.findIndex(res=>{return res.id==star.id})
if(index>-1){
return true
}else{
return false
}
}
}
},
props: {
defaultSign:{//回显签章的数据
type:Array,
default:[],
},
signDataList:{//选好签名 和签章的数据,可用于渲染按钮和拖到出图片的
type:Array,
default:[],
},
pdfUrl:{//pdf的路径
type: String,
default:null,
},
reportData: {
type: Object,
default: {},
},
id: {
type: String,
default: null,
},
handleType: {//操作类型, 是查看还是添加或者签章
type: String,
default: "add",
},
curfilesListId: {
type: String,
default: null,
},
},
data() {
return {
basicUrl: import.meta.env.VITE_APP_BASE_API,
loading:true,
signList:[],
filesList:[],
sliderShow:false,sliderValue:150,
curNode:null,
orderId:null,
filesListIndex:0,
nemeList:[],
signaturePositions: [],
userSignDto:[],
isPdfLoading: false,
pdfDoc:null,
pdfurls:null,
pages: [],//pdfjs插件生成的总共页数
currentPage: 1,//pdf当前的页码
clientPageX: 0,
clientPageY: 0,
num: 0,//签章的第几个
numName:0,//名字的第几个
dom: '',//当鼠标点下签章js创建div元素,用于存储签章的图片,样式,位置等临时dom内容
ishightlight: null,//签章按钮高亮的判断标识
namehightlight: null,//签名按钮高亮的判断标识
signatureList:[],//选择后的签章,即需要拖拽的签章集合
active:0,//控制页面上一页下一页
uploadFileList:[],
uploadForm:{
organId:'',
auditId:'',
appId:'',
signNames:[],
},
reportAudit:[],
authorizerList:[],
organId:'',
auditId:'',
appId:'',
allUserObj:{},
};
},
methods: {
//isDedaultSign 是判断是否是回显的数据调用的这个接口
sealPic(item, i,isDedaultSign) {
this.dom = document.createElement('div')
this.num=this.num+1
this.dom.setAttribute('class', 'seal-img mark ' + item.id + ' '+item.type+' ' + this.num)
this.dom.setAttribute('id',this.num)
if(isDedaultSign){//如果是打开这个页面时有回显的数据,那么走这里
this.dom.setAttribute('style', 'position: absolute;left:'+item.sxaxis+ 'px;top:'+item.syaxis+'px')
this.dom.innerHTML = `
<div id="zoomPicture" style="cursor:pointer;"></div>
<div id="delgongzhang"> ×</div>
<div id="suregongzhang"> √</div>
<img id="qz-img" src="${import.meta.env.VITE_APP_BASE_API + item.file}" class="${item.file}" style="height:${item.height}px">
<div id="qz-img-cover" style="width:100%;height:100%;position: absolute;left: 0px;top: 0px"></div>`
this.dom.style.height = item.height+'px'
}else{//这里是点击左侧签章按钮时根据签章的信息生成一个dom,然后给DOM加上删除 确定 签章图片 样式等
this.dom.src = item.file
item.isActive=true
this.ishightlight = i
this.dom.innerHTML = `
<div id="zoomPicture" style="cursor:pointer;"></div>
<div id="delgongzhang"> ×</div>
<div id="suregongzhang">√</div>
<img id="qz-img" src="${import.meta.env.VITE_APP_BASE_API+item.file}" class="${item.file}">
<div id="qz-img-cover" style="width:100%;height:100%;position: absolute;left: 0;top: 0"></div> `
}
// 鼠标抬开
const dom1 = this.dom.querySelector('#delgongzhang')
const dom2 = this.dom.querySelector('#suregongzhang')
const dom3 = this.dom.querySelector('#zoomPicture')
const appendDemo=()=>{
document.querySelector('#pdf-box' + this.currentPage).appendChild(this.dom)
document.onmousemove = null
document.onmouseup = null
this.dom.onmousedown = this.moveTo //创建的这个DOM鼠标按下时
dom1.onclick = this.onclcikdel
dom2.onclick = this.onclciksure
dom3.onclick = this.onclcikZoom //点击放大缩小的按钮触发的方法
this.dom.onmouseenter = this.mouseenter//鼠标指上这个DOM时,× √显示
this.dom.onmouseleave = this.mouseleave//鼠标离开这个DOM时× √隐藏
}
if(isDedaultSign){
appendDemo()
}else{
document.onmouseup = e => {
appendDemo()
}
}
},
moveTo(e) {
if (e.target.id ==='qz-img-cover') {
e.target.parentNode.childNodes[5].style.display = 'block'//章子图片上的确定图标显示
e.target.style.boxShadow = 'none'
const odiv = e.currentTarget
const disX = e.clientX - odiv.offsetLeft//鼠标x轴的位置-目标元素离父元素的左边距离
const disY = e.clientY - odiv.offsetTop//鼠标Y轴的位置-目标元素离父元素的上边距离
//给点击的当前DOM计算距左距上的style位置
document.onmousemove = e => {
e.preventDefault();
let left = e.clientX - disX
let top = e.clientY - disY
if (left <= 0) {
left = 0
} else if (left >= document.querySelector('#pdf-box' + this.currentPage).clientWidth - odiv.clientWidth) {
left = document.querySelector('#pdf-box' + this.currentPage).clientWidth - odiv.clientWidth
} else {
left = left - 10
}
if (top <= 0) {
top = 0
} else if (top >= document.querySelector('#pdf-box' + this.currentPage).clientHeight - odiv.clientHeight) {
top = document.querySelector('#pdf-box' + this.currentPage).clientHeight - odiv.clientHeight
} else {
top = top - 10
}
odiv.style.left = left + 'px'
odiv.style.top = top + 'px'
}
document.onmouseup = e => {
if (e.target.id !== 'qz-img') {
document.onmousemove = null
document.onmouseup = null
return
}
e.target.parentNode.childNodes[3].style.display = 'block'
e.target.parentNode.childNodes[5].style.display = 'block'
e.target.parentNode.style.boxShadow = 'none'
e.target.parentNode.children[0].style.display = 'none'
let index = null
this.signaturePositions.map((item, i) => {
if (item.num === this.currentPage + '' && item.id ===e.target.parentNode.classList[2] && item.signNum === e.target.parentNode.classList[3]) {
index = i
}
})
if (index) {
this.signaturePositions.splice(index, 1)
}
if (index === 0) {
this.signaturePositions.splice(index, 1)
}
this.clientPageX = Number(e.clientX - disX)
this.clientPageY = Number(e.clientY - disY)
document.onmousemove = null
document.onmouseup = null
}
}
},
onclciksure(e) {
this.sliderShow=false
e.target.parentNode.childNodes[5].style.display = 'none'
const odiv = e.target.parentNode // 获取目标元素
// 算出鼠标相对元素的位置
const clientX = e.clientX - 50
const clientY = e.clientY - 50
const disX = clientX - odiv.offsetLeft
const disY = clientY - odiv.offsetTop
let boom = false
let index = null
if (e.target.parentNode.classList[2]) {//章子上确定按钮的class里有页码 有id
// if (e.path[1].classList[3] && e.path[1].classList[2]) {//章子上确定按钮的class里有页码 有id
this.signaturePositions.map((item, i) => {
if (item.num === this.currentPage + '' && item.id ===e.target.parentNode.classList[2] && item.signNum === (e.target.parentNode.classList[4]||e.target.parentNode.id)) {
boom = true
index = i
}
})
const width = odiv.childNodes[7].offsetWidth;
const height = odiv.childNodes[7].offsetHeight;
if (boom) {
this.signaturePositions.splice(index, 1, {
signNum:e.target.parentNode.classList[4]||e.target.parentNode.id,//第几个章
id: e.target.parentNode.classList[2],
sxaxis: Number(clientX - disX),
syaxis: Number(clientY - disY),
num: this.currentPage + '',
width:width,
height:height,
type: e.target.parentNode.classList[3],
file:e.target.nextSibling.nextSibling.className,
})
} else {
this.signaturePositions.push({
signNum:e.target.parentNode.classList[4]||e.target.parentNode.id,//从class类名得到是第几个章,章的顺序
id: e.target.parentNode.classList[2],//对钩上的class为id的
sxaxis: Number(clientX - disX),
syaxis: Number(clientY - disY),
num: this.currentPage + '',//第几页
width:width,
height:height,
type: e.target.parentNode.classList[3],
file:e.target.nextSibling.nextSibling.className,
})
}
}
},
onclcikdel(e) {
this.sliderShow=false
e.target.parentNode.style.display = 'none'//章子上删除按钮影藏
let index = null
this.signaturePositions.map((item, i) => {
if (item.num === this.currentPage + '' && item.id ===e.target.parentNode.classList[2] && item.signNum === (e.target.parentNode.classList[4]||e.target.parentNode.id)) {
index = i
}
})
if (index) {
this.signaturePositions.splice(index, 1)
}
if (index === 0) {
this.signaturePositions.splice(index, 1)
}
},
mouseenter(e) {
e.currentTarget.childNodes[1].style.display = 'block'
e.currentTarget.childNodes[3].style.display = 'block'
},
mouseleave(e) {
e.currentTarget.childNodes[1].style.display = 'none'
e.currentTarget.childNodes[3].style.display = 'none'
},
changeSlider(val){
this.curNode.target.parentNode.style.height=val+'px'
this.curNode.target.parentNode.style.border="1px solid red"
this.curNode.target.parentNode.children[3].style.height=val+'px'
let zoomWidth = 40-(((150-val)/10)*2);
if(zoomWidth < 20){
zoomWidth = 20;
}
this.curNode.target.parentNode.children[0].style.height=zoomWidth+'px'
this.curNode.target.parentNode.children[0].style.width=zoomWidth+'px'
},
onclcikZoom(e){
this.curNode = e
this.sliderValue = e.target.parentNode.offsetHeight;
this.sliderShow=true
let borderArr =document.getElementsByClassName('mark')
for (let index = 0; index < borderArr.length; index++) {
const element = borderArr[index];
element.style.border='none'
}
},
handleNextPage() {
if (this.currentPage < this.pages) {
this.currentPage++
let curPageData=this.defaultSign[0]&&this.defaultSign.filter(res=>{return res.num==this.currentPage})
curPageData.map((sign, index) => {
this.sealPic(sign, index,true)
})
} else {
this.$message.warning('已经是最后一页了')
}
},
handleUpPage() {
if (this.currentPage > 1) {
this.currentPage--
} else {
this.$message.warning('已经是第一页了')
}
},
_loadFile(url) {
if(url){
//根据传进来的pdf路径, 解析pdf,
PDFJS.getDocument(url).promise.then((pdf) => {
this.pdfDoc = pdf
this.pages = pdf.numPages
this.$nextTick(res=>{
this._renderPage(1,pdf)
})
})
}
},
//渲染pdf
_renderPage(num,pdf) {
this.isPdfLoading = true
pdf.getPage(num).then((page) => {
const canvas = document.getElementById('the-canvas' + num)
if(!canvas){return }
const ctx = canvas.getContext('2d')
const dpr = window.devicePixelRatio || 1//当前显示设备的物理像素分辨率与CSS像素分辨率的比值
const bsrs =
ctx.webkitBackingStorePixelRatio ||
ctx.mozBackingStorePixelRatio ||
ctx.msBackingStorePixelRatio ||
ctx.oBackingStorePixelRatio ||
ctx.backingStorePixelRatio ||
1
const ratio = dpr / bsrs//设备像素比
// const viewport = page.getViewport(
// screen.availWidth / page.getViewport(1).width
// )
const viewport = page.getViewport({ scale: 1 })
/*
*将 canvas 的高和宽分别乘以 ratio 将其放大,
*然后再用 css 将其样式高和宽限制成初始的大小。
*/
canvas.width = viewport.width * ratio
canvas.height = viewport.height * ratio
// canvas.style.width = viewport.width * 0.5 + 'px'
// canvas.style.height = viewport.height * 0.5 + 'px'
// canvas.style.width = 21 + 'cm'
// canvas.style.height = 29.7 + 'cm'
ctx.setTransform(ratio, 0, 0, ratio, 0, 0)
const renderContext = {
canvasContext: ctx,
viewport: viewport
}
page.render(renderContext);
if (this.pages > num) {
this._renderPage(num + 1,pdf)
}
})
.finally(() => {
this.isPdfLoading = false
})
},
upStep(){
this.$confirm('点击确定后拖动过的章子或签名无法回显,是否确定上一步', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.active=this.active-1
}).catch(() => {
})
},
// 提交
handleSubmit() {
if (this.signaturePositions.length === 0) {
this.$message.warning('请选择签章')
return
}
this.$emit("submitSign",this.signaturePositions,this.curfilesListId);
return
},
},
mounted(){
this._loadFile(this.getPDFurl);
setTimeout(() => {
// 签章回显
if (this.defaultSign.length > 0) {
this.signaturePositions=this.defaultSign
let curPageData=this.defaultSign[0]&&this.defaultSign.filter(res=>{return res.num=='1'})
curPageData.map((sign, index) => {
this.sealPic(sign, index,true)
})
}
}, 200);
},
created() {
},
};
</script>
<style scoped lang="scss">
.my-dialog-footer{
top:10px;
}
.sign-img-box{
display: flex;
flex-direction: column;
.labelWidth{
min-width: 40px;
}
.btnsWrapper{
display: flex;
flex-flow: wrap;
}
.signBtns{
display: flex;
flex-direction: row;
}
>div{
display: flex;
flex-direction: row;
}.sign-img{
margin: 10px;
line-height: 30px;
font-size: 14px;
.common-button-hightlight{
// padding:0 21px;
height: 32px;
background-color: #409eff;
font-size: 14px;
color: #ffffff;
//border:1px solid $light-button-color;
}
}
}
.pdf-box{
position: relative;
.pageTab{
position: absolute;
display: flex;
align-items: center;
right: 0px;
top: -20px;
i{
cursor:pointer;
}
i:hover{
color:#1890ff;
}
}
}
.sliderWrapper{
position: absolute;
top: -35px;
// width: 50%;
}
:deep(div){
.seal-img{
position:absolute;
top:0px;
left:0px;
height:150px;
}
#zoomPicture{
position: absolute;
top:0px;
left:0px;
z-index:999;
background: rgba(0, 0, 0, .8);
padding: 5px 5px 2px;
border-radius: 4px;
display: none;
background-image: url('@/assets/images/zoom.png');
width: 40px;
height: 40px;
background-position: center;
background-repeat: no-repeat;
background-size: 80%;
}
#delgongzhang{
position: absolute;
top:0px;
right:0px;
width:20px;
height: 20px;
z-index:999;
font-weight: 800;
text-align: center;
line-height: 16px;
border-radius: 50%;
cursor: pointer;
}
#suregongzhang{
position: absolute;
bottom:0px;
right:0px;
width:20px;
height: 20px;
z-index:999;
border: 2px solid green;
color: green;
font-weight: 900;
text-align: center;
border-radius: 50%;
line-height: 19px;
cursor: pointer;
}
#qm-img-cover{
width:100%;height:100%;position: absolute;left: 0;top: 0;
}
#qz-img{
height:150px;
cursor:pointer;
backgroundSize:cover;
borderRadius:50%;
box-shadow: 0 8px 16px #ccc;
}
#qm-img{
height:150px;cursor:pointer;backgroundSize:cover;borderRadius:50%;box-shadow: 0 8px 16px #ccc;
}
}
</style>