实现pdf盖章拖拽组件开发

可直接cv

效果如下(尺寸问题进行了缩放):

技术框架: vue3+ts  

使用插件:fabric,vuedraggable,pdfjs-dist,lodash,对应版本如下:

npm i fabric --save
npm i -S vuedraggable
npm install pdfjs-dist
npm i --save lodash

组件代码如下:

使用注意如下: 该组件接受两个参数pdfFlow和imgList;

1. pdfFlow: 这里的pdfFlow即pdf的链接地址

2.imgList: 盖章图片的list,格式参考如下:图片的参数key为img,如果是其他参数名,调用addSeal函数的第一个参数需要对应修改

const imgList = ref<any>([
  {
    img: `${window.origin}/src/assets/zhang.png`,
    id: '1',
  },
  {
    img: `${window.origin}/src/assets/zhang.png`,
    id: '2',
  },
  {
    img: `${window.origin}/src/assets/zhang.png`,
    id: '3',
  },
  {
    img: `${window.origin}/src/assets/zhang.png`,
    id: '4',
  },
  {
    img: `${window.origin}/src/assets/zhang.png`,
    id: '5',
  },
  {
    img: `${window.origin}/src/assets/zhang.png`,
    id: '6',
  },
  {
    img: `${window.origin}/src/assets/zhang.png`,
    id: '7',
  },
])
<template>
  <div id="elesign" class="elesign">
    <el-row>
      <el-col :span="4" style="margin-top: 1%">
        <div class="left-title">我的印章</div>
        <div style="max-height: 700px; overflow: auto" class="img_list">
          <!-- <img class="imgstyle" width="100%;" v-for="item in mainImagelist" :src="item" /> -->
          <draggable :list="mainImagelist" animation="300" :sort="false" @end="end">
            <template #item="{ element }">
              <div class="item">
                <img :src="element.img" width="100%;" height="100%" class="imgstyle" />
              </div>
            </template>
          </draggable>
        </div>
      </el-col>
      <el-col :span="20" style="text-align: center" class="pCenter">
        <div class="page">
          <el-button class="btn-outline-dark" @click="prevPage">上一页</el-button>
          <el-button class="btn-outline-dark" @click="nextPage">下一页</el-button>
          <el-button class="btn-outline-dark">{{ pageNum }}/{{ numPages }}页</el-button>
          <el-input-number
            style="margin: 0 5px; border-radius: 5px"
            class="btn-outline-dark"
            v-model="pageNum"
            :min="1"
            :max="numPages"
            label="输入页码"
          ></el-input-number>
          <el-button class="btn-outline-dark" @click="cutover">跳转</el-button>
        </div>
        <canvas id="the-canvas" />
        <!-- 盖章部分 -->
        <canvas id="ele-canvas"></canvas>
        <div class="ele-control" style="margin-bottom: 2%">
          <el-button class="btn-outline-dark" @click="removeSignature"> 删除签章</el-button>
          <el-button class="btn-outline-dark" @click="clearSignature"> 清除所有签章</el-button>
        </div>
      </el-col>
    </el-row>
  </div>
</template>
<script setup lang="ts">
// @ts-ignore
import { fabric } from 'fabric'
import draggable from 'vuedraggable'
import * as PDFJS from 'pdfjs-dist'
import { uniq, uniqBy } from 'lodash'
// @ts-ignore
const workerSrc = import('pdfjs-dist/build/pdf.worker.entry')
// @ts-ignore
PDFJS.GlobalWorkerOptions.workerSrc = workerSrc
let pdfUrl = ref<string>('')
let pdfDoc = ref<any>(null)
let numPages = ref<number>(1)
let pageNum = ref<number>(1)
let scale = ref<number>(1.4)
let pageRendering = ref<Boolean>(false)
let pageNumPending = ref<any>(null)
let sealUrl = ref<string>('')
let signUrl = ref<string>('')
let canvas = ref<any>(null)
let ctx = ref<any>(null)
let canvasEle = ref<any>(null)
let whDatas = ref<any>(null)
let width = ref<number>(0)
let height = ref<number>(0)
// let mainImagelist = ref<any>([])

const props = defineProps<{
  pdfFlow: string
  imgList: any
}>()

const mainImagelist = computed(() => {
  return props.imgList
})

onMounted(() => {
  let canvaEle: any = document.querySelector('#ele-canvas')
  canvasEle.value = new fabric.Canvas(canvaEle)

  pdfUrl.value = props.pdfFlow
  showpdf(pdfUrl.value)
  // setPdfArea()
})

watch(
  () => whDatas.value,
  () => {
    if (whDatas.value) {
      renderFabric()
      canvasEvents()
      let eleCanvas: any = document.querySelector('#ele-canvas')
      // eleCanvas.style = 'border:1px solid #5ea6ef'
    }
  },
  { deep: true }
)

watch(
  () => pageNum.value,
  () => {
    commonSign(pageNum.value)
    queueRenderPage(pageNum.value)
  },
  { deep: true }
)

watch(
  () => props.pdfFlow,
  () => {
    pdfUrl.value = props.pdfFlow
    showpdf(pdfUrl.value)
  },
  { deep: true }
)

const renderPage = (num: any) => {
  pageRendering.value = true
  // 使用 toRaw的原因: https://www.jianshu.com/p/1432ccd5089a
  return toRaw(pdfDoc.value)
    .getPage(num)
    .then((page: any) => {
      let viewport = page.getViewport({ scale: scale.value }) //设置视口大小

      width.value = viewport.width > width.value ? viewport.width : width.value
      height.value = viewport.height > height.value ? viewport.height : height.value
      
      // pdf 区域 (取最大的页面的尺寸 pdf页面大小不同会导致拖拽区域偏差)
      canvas.value.width = width.value
      canvas.value.height = height.value
      
      // Render PDF page into canvas context
      let renderContext = {
        canvasContext: ctx.value,
        viewport: viewport,
      }
      let renderTask = page.render(renderContext)
      // Wait for rendering to finish
      renderTask.promise.then(() => {
        pageRendering.value = false
        if (pageNumPending.value !== null) {
          // New page rendering is pending
          renderPage(pageNumPending.value)
          pageNumPending.value = null
        }
      })
    })
}

// 生成绘图区域
const renderFabric = () => {
  let canvaEle: any = document.querySelector('#ele-canvas')
  let pCenter: any = document.querySelector('.pCenter')
  canvaEle.width = pCenter.clientWidth
  canvaEle.height = whDatas.value.height

  canvasEle.value = new fabric.Canvas(canvaEle)
  let container: any = document.querySelector('.canvas-container')
  container.style.position = 'absolute'
  container.style.top = '50px'
  // container.style.left = "30%";
}

const queueRenderPage = (num: any) => {
  if (pageRendering.value) {
    pageNumPending.value = num
  } else {
    renderPage(num)
  }
}

const prevPage = () => {
  confirmSignature()
  if (pageNum.value <= 1) {
    return
  }
  pageNum.value = pageNum.value - 1
}

const nextPage = () => {
  confirmSignature()
  if (pageNum.value >= numPages.value) {
    return
  }
  pageNum.value = pageNum.value + 1
}

const cutover = () => {
  confirmSignature()
}

//渲染pdf,到时还会盖章信息,在渲染时,同时显示出来,不应该在切换页码时才显示印章信息
const showpdf = (pdfUrl: any) => {
  canvas.value = document.getElementById('the-canvas')
  let caches = JSON.parse(localStorage.getItem('signs') as any) //获取缓存字符串后转换为对象
  ctx.value = canvas.value.getContext('2d')
  PDFJS.getDocument({ url: pdfUrl, rangeChunkSize: 65536, disableAutoFetch: false, cMapUrl: '../../../public/static/cmaps/' }).promise.then((pdfDoc_) => {
    pdfDoc.value = pdfDoc_
    numPages.value = pdfDoc.value.numPages
    renderPage(pageNum.value).then(() => {
      renderPdf({
        width: canvas.value.width,
        height: canvas.value.height,
      })
    })
    commonSign(pageNum.value, true)
    nextTick(() => {
      if (caches != null) {
        let datas = caches[pageNum.value]
        if (datas != null && datas != undefined) {
          for (let index in datas) {
            addSeal(datas[index].sealUrl.img, datas[index].left, datas[index].top, datas[index].index)
          }
        }
      }
    })
  })
}

/**
 *  盖章部分开始
 */
// 设置绘图区域宽高
const renderPdf = (data: any) => {
  whDatas.value = data
}

// 相关事件操作
const canvasEvents = () => {
  // 拖拽边界 不能将图片拖拽到绘图区域外
  canvasEle.value.on('object:moving', function (e: any) {
    var obj = e.target
    // if object is too big ignore
    if (obj.currentHeight > obj.canvas.height || obj.currentWidth > obj.canvas.width) {
      return
    }
    obj.setCoords()
    // top-left  corner
    if (obj.getBoundingRect().top < 0 || obj.getBoundingRect().left < 0) {
      obj.top = Math.max(obj.top, obj.top - obj.getBoundingRect().top)
      obj.left = Math.max(obj.left, obj.left - obj.getBoundingRect().left)
    }
    // bot-right corner
    if (
      obj.getBoundingRect().top + obj.getBoundingRect().height > obj.canvas.height ||
      obj.getBoundingRect().left + obj.getBoundingRect().width > obj.canvas.width
    ) {
      obj.top = Math.min(obj.top, obj.canvas.height - obj.getBoundingRect().height + obj.top - obj.getBoundingRect().top)
      obj.left = Math.min(obj.left, obj.canvas.width - obj.getBoundingRect().width + obj.left - obj.getBoundingRect().left)
    }
  })
}

// 添加公章
const addSeal = (sealUrl: any, left: any, top: any, index: any) => {
  fabric.Image.fromURL(sealUrl, (oImg: any) => {
    oImg.set({
      left: left,
      top: top,
      // angle: 10,
      scaleX: 0.8,
      scaleY: 0.8,
      index: index,
    })
    oImg.scale(0.2) //图片缩小一
    canvasEle.value.add(oImg)
  })
}

// 删除签章
const removeSignature = () => {
  canvasEle.value.remove(canvasEle.value.getActiveObject())
}

//翻页展示盖章信息
const commonSign = (pageNum: any, isFirst = false) => {
  if (isFirst == false) canvasEle.value.remove(canvasEle.value.clear()) //清空页面所有签章
  let caches = JSON.parse(localStorage.getItem('signs') as any) //获取缓存字符串后转换为对象
  if (caches != null) {
    let datas = caches[pageNum]
    if (datas != null && datas != undefined) {
      for (let index in datas) {
        addSeal(datas[index].sealUrl.img, datas[index].left, datas[index].top, datas[index].index)
      }
    }
  }
}

//去重判断
const iteratee = (item: any) => {
  return item.width + '|' + item.height + '|' + item.top + '|' + item.left;  
};

//确认签章位置并保存到缓存
const confirmSignature = () => {
  let data = canvasEle.value.getObjects() //获取当前页面内的所有签章信息
  let caches: any = JSON.parse(localStorage.getItem('signs') as any) //获取缓存字符串后转换为对象
  data = uniqBy(data, iteratee)
  let signDatas: any = {} //存储当前页的所有签章信息
  let i = 0
  // let sealUrl = '';
  
  for (let val of data) {

    // 超出pdf区域的坐标信息不缓存
    if (val.left > width.value || val.top > height.value) {
      break;
    }
    signDatas[i] = {
      width: val.width,
      height: val.height,
      top: val.top,
      left: val.left,
      angle: val.angle,
      translateX: val.translateX,
      translateY: val.translateY,
      scaleX: val.scaleX,
      scaleY: val.scaleY,
      pageNum: pageNum.value,
      sealUrl: mainImagelist.value[val.index],
      index: val.index,
    }
    i++
  }
  if (caches == null) {
    caches = {}
    caches[pageNum.value] = signDatas
  } else {
    caches[pageNum.value] = signDatas
  }
  localStorage.setItem('signs', JSON.stringify(caches)) //对象转字符串后存储到缓存
}

//清空数据
const clearSignature = () => {
  canvasEle.value.remove(canvasEle.value.clear()) //清空页面所有签章
  localStorage.removeItem('signs') //清除缓存
}

const end = (e: any) => {
  addSeal(mainImagelist.value[e.newDraggableIndex].img, e.originalEvent.layerX, e.originalEvent.layerY, e.newDraggableIndex)
}

defineExpose({ confirmSignature, clearSignature })
</script>
<style lang="scss" scoped>
.pCenter {
  overflow-x: hidden;
}
#the-canvas {
  margin-top: 10px;
}

html:fullscreen {
  background: white;
}
.elesign {
  display: flex;
  flex: 1;
  flex-direction: column;
  position: relative;
  /* padding-left: 180px; */
  margin: auto;
  /* width:600px; */
}
.page {
  text-align: center;
  margin: 0 auto;
  height: 36px;
}
#ele-canvas {
  /* border: 1px solid #5ea6ef; */
  overflow: hidden;
  mix-blend-mode: multiply !important;
}
.ele-control {
  text-align: center;
  margin-top: 3%;
}
#page-input {
  width: 7%;
}

@keyframes ani-demo-spin {
  from {
    transform: rotate(0deg);
  }
  50% {
    transform: rotate(180deg);
  }
  to {
    transform: rotate(360deg);
  }
}
/* .loadingclass{
    position: absolute;
    top:30%;
    left:49%;
    z-index: 99;
} */
.left {
  position: absolute;
  top: 42px;
  left: -5px;
  padding: 5px 5px;
  /*border: 1px solid #eee;*/
  /*border-radius: 4px;*/
}
.left-title {
  text-align: center;
  padding-bottom: 10px;
  border-bottom: 1px solid #eee;
  height: 36px;
}
li {
  list-style-type: none;
  padding: 10px;
}
.imgstyle {
  vertical-align: middle;
  // width: 130px;
  border: solid 1px #e8eef2;
  background-repeat: no-repeat;
  margin-bottom: 10px;
  // mix-blend-mode: multiply;
}
.right {
  position: absolute;
  top: 7px;
  right: -177px;
  margin-top: 34px;
  padding-top: 10px;
  padding-bottom: 20px;
  width: 152px;
  /*border: 1px solid #eee;*/
  /*border-radius: 4px;*/
}
.right-item {
  margin-bottom: 15px;
  margin-left: 10px;
}
.right-item-title {
  color: #777;
  height: 20px;
  line-height: 20px;
  font-size: 12px;
  font-weight: 400;
  text-align: left !important;
}
.detail-item-desc {
  color: #333;
  line-height: 20px;
  width: 100%;
  font-size: 12px;
  display: inline-block;
  text-align: left;
}
.btn-outline-dark {
  color: #000 !important;
  background-color: transparent;
  background-image: none;
  border: 1px solid #3e4b5b;
}

.btn-outline-dark:hover {
  color: #fff !important;
  background-color: #3e4b5b;
  border-color: #3e4b5b;
}
.img_list::-webkit-scrollbar {
  width: 3px !important; /*高宽分别对应横竖滚动条的尺寸*/
  height: 8px !important;
}

.img_list::-webkit-scrollbar-thumb {
  /*滚动条里面小方块*/
  /*border-radius: 10px;
    box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2);
    background: rgba(23,161,230,1);*/
  /*滚动条里面小方块*/
  border-radius: 8px;
  background: #3e4b5b;
  /*background-color: skyblue;*/
  /*background-image: -webkit-linear-gradient( 45deg, rgba(255, 255, 255, 0.2) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.2) 50%, rgba(255, 255, 255, 0.2) 75%, transparent 75%, transparent );*/
}

.img_list::-webkit-scrollbar-track {
  /*滚动条里面轨道*/
  box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2);
  border-radius: 10px;
  background: #ededed;
}

#ele-canvas {
  // opacity: 0.5;
}
</style>
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值