vue2 之 实现pdf电子签章

一、前情提要

1. 需求

仿照e签宝,实现pdf电子签章 => 拿到pdf链接,移动章的位置,获取章的坐标

技术 : 使用fabric + pdfjs-dist + vuedraggable

2. 借鉴

一位大佬的代码仓亏 : 地址

一位大佬写的文章 :地址

3. 优化

在大佬的代码基础上,进行了些许优化,变的更像e签宝

二、下载

ps : 怕版本不同,导致无法运行,请下载指定版本

1. fabric

fabric : 是一个功能强大且操作简单的 Javascript HTML5 canvas 工具库

javascript 复制代码
npm install fabric@5.3.0

2. pdfjs-dist

javascript 复制代码
npm install pdfjs-dist@2.5.207

问题一

注意 : 最好配置一下babel,因为****打包的时候可能会报错

因为babel默认不会转化node_modules中的包,但是****pdfjs-dist用了es6的东东

javascript 复制代码
// 安装包
npm install babel-loader @babel/core @babel/preset-env -D

在webpack.config.js中配置

javascript 复制代码
{
  test: /\.js$/,
  loader: 'babel-loader',
  include: [
    resolve('src'),
    // 转化pdfjs-dist,之所以分开写,是因为pdfjs-dist里面有很多es6的语法,但是我们只需要转化pdfjs-dist里面的web文件夹下的js文件
    resolve('node_modules/pdfjs-dist/web/pdf_viewer.js'),
    resolve('node_modules/pdfjs-dist/build/pdf.js'),
    resolve('node_modules/pdfjs-dist/build/pdf.worker.js'),
    resolve('node_modules/pdfjs-dist/build/pdf.worker.entry.js')        ]
},

问题二

pdf.js文件过大,可以给 .babelrc 加上属性,****"compact": false

3. vuedraggable

javascript 复制代码
npm install vuedraggable@2.24.3

三、代码

1. 准备pdf文件

text.pdf 可放置在 src/static 文件夹中

ps : 线上最好让后端返回pdf链接,因为存在pdf跨域问题

2. 大佬的代码

javascript 复制代码
<!-- //?模块说明 =>  合同签章模块 -->
<template>
  <div id="elesign" class="elesign">
    <el-row>
      <el-col :span="4" style="margin-top: 1%">
        <div class="left-title">我的印章</div>
        <draggable
          v-model="mainImagelist"
          :group="{ name: 'itext', pull: 'clone' }"
          :sort="false"
          @end="end"
        >
          <transition-group type="transition">
            <li v-for="item in mainImagelist" :key="item" class="item" style="text-align: center">
              <img :src="item" width="100%;" height="100%" class="imgstyle" />
            </li>
          </transition-group>
        </draggable>
      </el-col>
      <el-col :span="16" style="text-align: center" class="pCenter">
        <div class="page">
          <!-- <el-button class="btn-outline-dark" @click="zoomIn">-</el-button>
          <span style="color: red">{{ (percentage * 100).toFixed(0) + '%' }}</span>
          <el-button class="btn-outline-dark" @click="zoomOut">+</el-button> -->
          <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>
          <el-button class="btn-outline-dark" @click="submitSignature">提交所有签章信息</el-button>
        </div>
      </el-col>
      <el-col :span="4" style="margin-top: 1%">
        <div class="left-title">任务信息</div>
        <div style="text-align: center">
          <div>
            <div class="right-item">
              <div class="right-item-title">文件主题</div>
              <div class="detail-item-desc">{{ taskInfo.title }}</div>
            </div>
            <div class="right-item">
              <div class="right-item-title">发起方</div>
              <div class="detail-item-desc">{{ taskInfo.uname }}</div>
            </div>
            <div class="right-item">
              <div class="right-item-title">截止时间</div>
              <div class="detail-item-desc">{{ taskInfo.endtime }}</div>
            </div>
          </div>
        </div>
      </el-col>
    </el-row>
  </div>
</template>
<script>
import draggable from 'vuedraggable';
import { fabric } from 'fabric';
import workerSrc from 'pdfjs-dist/es5/build/pdf.worker.entry';
import * as pdfjsViewer from 'pdfjs-dist/web/pdf_viewer';
const pdfjsLib = require('pdfjs-dist/es5/build/pdf.js');
pdfjsLib.GlobalWorkerOptions.workerSrc = workerSrc;
export default {
  components: { draggable },
  data() {
    return {
      // pdf预览
      pdfUrl: '',
      pdfDoc: null,
      numPages: 1,
      pageNum: 1,
      scale: 2.2,
      pageRendering: false,
      pageNumPending: null,
      sealUrl: '',
      signUrl: '',
      canvas: null,
      ctx: null,
      canvasEle: null,
      whDatas: null,
      mainImagelist: [],
      taskInfo: {}
      // percentage: 1
    };
  },
  computed: {
    hasSigna() {
      if (this.canvasEle && this.canvasEle.getObjects()[0]) {
        return true;
      } else {
        return false;
      }
    }
  },
  created() {
    var that = this;
    that.mainImagelist = [require('@/assets/img/projectCenter/sign.png'), require('@/assets/img/projectCenter/seal.png')];
    that.taskInfo = { title: '测试盖章', uname: '张三', endtime: '2021-09-01 17:59:59' };
    this.setPdfArea();
  },
  mounted() {
    // this.showpdf(this.pdfUrl);
    if (!pdfjsLib.getDocument || !pdfjsViewer.PDFViewer) {
      // eslint-disable-next-line no-alert
      alert('Please build the pdfjs-dist library using\n  `gulp dist-install`');
    }
  },
  methods: {
    // pdf预览
    // zoomIn() {
    //   console.log('缩小');
    //   if (this.scale <= 0.5) {
    //     this.$message.error('已经显示最小比例');
    //   } else {
    //     this.scale -= 0.1;
    //     this.percentage -= 0.1;
    //     this.renderPage(this.pageNum);
    //     this.renderFabric();
    //   }
    // },
    // zoomOut() {
    //   console.log('放大');
    //   if (this.scale >= 2.2) {
    //     this.$message.error('已经显示最大比例');
    //   } else {
    //     this.scale += 0.1;
    //     this.percentage += 0.1;
    //     this.renderPage(this.pageNum);
    //     this.renderFabric();
    //   }
    // },
    renderPage(num) {
      let _this = this;
      this.pageRendering = true;
      return this.pdfDoc.getPage(num).then((page) => {
        let viewport = page.getViewport({ scale: _this.scale }); // 设置视口大小
        _this.canvas.height = viewport.height;
        _this.canvas.width = viewport.width;

        // Render PDF page into canvas context
        let renderContext = {
          canvasContext: _this.ctx,
          viewport: viewport
        };
        let renderTask = page.render(renderContext);
        // Wait for rendering to finish
        renderTask.promise.then(() => {
          _this.pageRendering = false;
          if (_this.pageNumPending !== null) {
            // New page rendering is pending
            this.renderPage(_this.pageNumPending);
            _this.pageNumPending = null;
          }
        });
      });
    },
    queueRenderPage(num) {
      if (this.pageRendering) {
        this.pageNumPending = num;
      } else {
        this.renderPage(num);
      }
    },
    prevPage() {
      this.confirmSignature();
      if (this.pageNum <= 1) {
        return;
      }
      this.pageNum--;
    },
    nextPage() {
      this.confirmSignature();
      if (this.pageNum >= this.numPages) {
        return;
      }
      this.pageNum++;
    },
    cutover() {
      this.confirmSignature();
    },
    // 渲染pdf,到时还会盖章信息,在渲染时,同时显示出来,不应该在切换页码时才显示印章信息
    showpdf(pdfUrl) {
      let caches = JSON.parse(localStorage.getItem('signs')); // 获取缓存字符串后转换为对象
      // console.log(caches);

      if (caches != null) {
        let datas = caches[this.pageNum];
        if (datas != null && datas != undefined) {
          for (let index in datas) {
            this.addSeal(
              datas[index].sealUrl,
              datas[index].left,
              datas[index].top,
              datas[index].index
            );
          }
        }
      }

      this.canvas = document.getElementById('the-canvas');
      this.ctx = this.canvas.getContext('2d');
      pdfjsLib
        .getDocument({ url: pdfUrl, rangeChunkSize: 65536, disableAutoFetch: false })
        .promise.then((pdfDoc_) => {
          this.pdfDoc = pdfDoc_;
          this.numPages = this.pdfDoc.numPages;
          this.renderPage(this.pageNum).then(() => {
            this.renderPdf({
              width: this.canvas.width,
              height: this.canvas.height
            });
          });
          this.commonSign(this.pageNum, true);
        });
    },
    /**
     *  盖章部分开始
     */
    // 设置绘图区域宽高
    renderPdf(data) {
      this.whDatas = data;
      // document.querySelector("#elesign").style.width = data.width + "px";
    },
    // 生成绘图区域
    renderFabric() {
      let canvaEle = document.querySelector('#ele-canvas');
      let pCenter = document.querySelector('.pCenter');
      canvaEle.width = pCenter.clientWidth;
      // canvaEle.height = (this.whDatas.height)*(this.scale);
      canvaEle.height = this.whDatas.height;

      this.canvasEle = new fabric.Canvas(canvaEle);
      let container = document.querySelector('.canvas-container');
      container.style.position = 'absolute';
      container.style.top = '50px';
      // container.style.left = "30%";
    },
    // 相关事件操作哟
    canvasEvents() {
      // 拖拽边界 不能将图片拖拽到绘图区域外
      this.canvasEle.on('object:moving', function (e) {
        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
          );
        }
      });
    },
    // 添加公章
    addSeal(sealUrl, left, top, index) {
      fabric.Image.fromURL(sealUrl, (oImg) => {
        oImg.set({
          left: left,
          top: top,
          // angle: 10,
          scaleX: 0.8,
          scaleY: 0.8,
          index: index
        });
        // oImg.scale(0.5); //图片缩小一
        this.canvasEle.add(oImg);
      });
    },
    // 删除签章
    removeSignature() {
      this.canvasEle.remove(this.canvasEle.getActiveObject());
    },
    // 翻页展示盖章信息
    commonSign(pageNum, isFirst = false) {
      if (isFirst == false) this.canvasEle.remove(this.canvasEle.clear()); // 清空页面所有签章
      let caches = JSON.parse(localStorage.getItem('signs')); // 获取缓存字符串后转换为对象
      // console.log(caches);
      if (caches == null) return false;
      let datas = caches[this.pageNum];
      if (datas != null && datas != undefined) {
        for (let index in datas) {
          this.addSeal(
            datas[index].sealUrl,
            datas[index].left,
            datas[index].top,
            datas[index].index
          );
        }
      }
    },
    // 确认签章位置并保存到缓存
    confirmSignature() {
      let data = this.canvasEle.getObjects(); // 获取当前页面内的所有签章信息
      let caches = JSON.parse(localStorage.getItem('signs')); // 获取缓存字符串后转换为对象

      let signDatas = {}; // 存储当前页的所有签章信息
      let i = 0;
      // let sealUrl = '';
      for (var val of data) {
        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: this.pageNum,
          sealUrl: this.mainImagelist[val.index],
          index: val.index
        };
        i++;
      }
      if (caches == null) {
        caches = {};
        caches[this.pageNum] = signDatas;
      } else {
        caches[this.pageNum] = signDatas;
      }
      localStorage.setItem('signs', JSON.stringify(caches)); // 对象转字符串后存储到缓存
    },
    // 提交数据
    submitSignature() {
      this.confirmSignature();
      // let caches = localStorage.getItem('signs');
      // console.log(JSON.parse(caches));
      return false;
    },
    // 清空数据
    clearSignature() {
      this.canvasEle.remove(this.canvasEle.clear()); // 清空页面所有签章
      localStorage.removeItem('signs'); // 清除缓存
    },
    end(e) {
      this.addSeal(
        this.mainImagelist[e.newDraggableIndex],
        e.originalEvent.layerX,
        e.originalEvent.layerY,
        e.newDraggableIndex
      );
    },
    // 设置PDF预览区域高度
    setPdfArea() {
      this.pdfUrl = './static/text.pdf';
      // this.pdfurl = res.data.data.pdfurl;
      this.$nextTick(() => {
        this.showpdf(this.pdfUrl); // 接口返回的应该还有盖章信息,不只是pdf
      });
    }
  },
  watch: {
    whDatas: {
      handler() {
        const loading = this.$loading({
          lock: true,
          text: 'Loading',
          spinner: 'el-icon-loading',
          background: 'rgba(0, 0, 0, 0.7)'
        });
        if (this.whDatas) {
          // console.log(this.whDatas);
          loading.close();
          this.renderFabric();
          this.canvasEvents();
          let eleCanvas = document.querySelector('#ele-canvas');
          eleCanvas.style = 'border:1px solid #5ea6ef;margin-top: 10px;';
        }
      }
    },
    pageNum: function () {
      this.commonSign(this.pageNum);
      this.queueRenderPage(this.pageNum);
    }
  }
};
</script>
<style lang="scss" scoped>
/*pdf部分*/
#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;
  margin-top: 1%;
}
#ele-canvas {
  /* border: 1px solid #5ea6ef; */
  overflow: hidden;
}
.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;
}
li {
  list-style-type: none;
  padding: 10px;
}
.imgstyle {
  vertical-align: middle;
  width: 130px;
  border: solid 1px #e8eef2;
  background-image: url('~@/assets/img/projectCenter/tuo.png');
  background-repeat: no-repeat;
}
.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: #0f1531;
  background-color: transparent;
  background-image: none;
  border: 1px solid #3e4b5b;
}

.btn-outline-dark:hover {
  color: #fff;
  background-color: #3e4b5b;
  border-color: #3e4b5b;
}
</style>

3. 优化后的代码

javascript 复制代码
<!-- //?模块说明 =>  合同签章模块 addToTab-->
<template>
  <div class="contract-signature-view">
    <div class="title-operation">
      <h2 class="title">合同签章</h2>
      <div class="operation">
        <el-button type="danger" @click="removeSignature">删除签章</el-button>
        <el-button type="danger" @click="clearSignature">清空签章</el-button>
        <el-button type="primary" @click="submitSignature">提交签章</el-button>
      </div>
    </div>
    <div class="section-box">
      <!-- 签章图片 -->
      <aside class="signature-img">
        <div class="info">
          <h3 class="name">印章</h3>
          <p class="text">将示例印章标识拖到文件相应区域即可获取签章位置</p>
        </div>
        <!-- 拖拽 -->
        <draggable
          v-model="mainImagelist"
          :group="{ name: 'itext', pull: 'clone' }"
          :sort="false"
          @end="end"
        >
          <transition-group type="transition">
            <li
              v-for="item in mainImagelist"
              :key="item.img"
              class="item"
              style="text-align: center"
            >
              <img :src="item.img" width="100%;" height="100%" class="img" />
            </li>
          </transition-group>
        </draggable>
      </aside>
      <!-- 主体区域 -->
      <section class="main-layout" :class="{ 'is-first': isFirst }">
        <!-- 操作 -->
        <div class="operate-box">
          <div class="slider-box">
            <el-slider
              class="slider"
              v-model="scale"
              :min="0.5"
              :max="2"
              :step="0.1"
              :show-tooltip="false"
              @change="sliderChange"
            />
            <span class="scale-value">{{ (scale * 100).toFixed(0) + '%' }}</span>
          </div>
          <div class="page-change">
            <i class="icon el-icon-arrow-left" @click="prevPage" />
            <!-- :min="1" -->
            <el-input
              class="input-box"
              v-model.number="pageNum"
              :max="defaultNumPages"
              @change="cutover"
            />
            <span class="default-text">/{{ defaultNumPages }}</span>
            <i class="icon el-icon-arrow-right" @click="nextPage" />
          </div>
        </div>
        <!-- 画图 -->
        <div class="out-view" :class="{ 'is-show': isShowPdf }">
          <div class="canvas-layout" v-for="item in numPages" :key="item">
            <!-- pdf部分 -->
            <canvas class="the-canvas" />
            <!-- 盖章部分 -->
            <canvas class="ele-canvas"></canvas>
          </div>
        </div>
        <i class="loading" v-loading="!isShowPdf" />
      </section>
      <!-- 位置信息 -->
      <div class="position-info">
        <h3 class="title">位置信息</h3>
        <ul class="nav">
          <li class="item" v-for="(item, index) in coordinateList" :key="index">
            <span>{{ item.name }}</span>
            <span>{{ item.page }}</span>
            <span>{{ item.left }}</span>
            <span>{{ item.top }}</span>
          </li>
        </ul>
      </div>
    </div>
  </div>
</template>
<script>
// 拖拽插件
import draggable from 'vuedraggable';
// pdf插件
import { fabric } from 'fabric';
import workerSrc from 'pdfjs-dist/es5/build/pdf.worker.entry';
const pdfjsLib = require('pdfjs-dist/es5/build/pdf.js');
pdfjsLib.GlobalWorkerOptions.workerSrc = workerSrc;

export default {
  components: { draggable },
  data() {
    return {
      // pdf地址
      pdfUrl: '',
      // 左侧签章列表
      mainImagelist: [],
      // 右侧坐标数据
      coordinateList: [{ name: '名称', page: '所在页面', left: 'x坐标', top: 'Y坐标' }],
      // 总页数
      numPages: 1,
      defaultNumPages: 1,
      // 当前页
      pageNum: 1,
      // 缩放比例
      scale: 1,
      // pdf是否显示
      isFirst: true,
      isShowPdf: false,
      // pdf最外层的out-view
      outViewDom: null,
      // 各页pdf的canvas-layout
      canvasLayoutTopList: [],
      // 用来签章的canvas数组
      canvasEle: [],
      // 绘图区域的宽高
      whDatas: null,
      // pdf渲染的canvas数组
      canvas: [],
      // pdf渲染的canvas的ctx数组
      ctx: [],
      // pdf渲染的canvas的宽高
      pdfDoc: null,
      // 隐藏的input,用来提交数据
      shadowInputValue: ''
    };
  },
  created() {
    this.mainImagelist = [
      { name: '印章', img: require('@/assets/img/projectCenter/contract-sign-img.png') }
      // { name: '印章', img: require('./sign.png') },
      // { name: '红章', img: require('@/assets/img/projectCenter/seal.png') }
    ];
    this.setPdfArea();
  },
  mounted() {},
  methods: {
    /**
     * pdf相关部分
     */
    // 设置PDF地址
    setPdfArea() {
      // // 1. 获取地址栏
      // const urlString = window.location.href;
      // // 2. 截取地址栏
      // const pdfStr = urlString.split('?')[1];
      // // 3. 截取pdf地址并解码
      // this.pdfUrl = decodeURIComponent(pdfStr.split('=')[1]);

      this.pdfUrl = './static/text.pdf';
      this.$nextTick(() => {
        this.showpdf(this.pdfUrl); // 接口返回的应该还有盖章信息,不只是pdf
      });
    },
    // 解析pdf
    showpdf(pdfUrl) {
      pdfjsLib
        .getDocument({ url: pdfUrl, rangeChunkSize: 65536, disableAutoFetch: false })
        .promise.then((pdfDoc_) => {
          this.pdfDoc = pdfDoc_;
          this.numPages = this.pdfDoc.numPages;
          this.defaultNumPages = this.pdfDoc.numPages;
          this.$nextTick(() => {
            this.canvas = document.querySelectorAll('.the-canvas');
            this.canvas.forEach((item) => {
              this.ctx.push(item.getContext('2d'));
            });
            // 循环渲染pdf
            for (let i = 1; i <= this.numPages; i++) {
              this.renderPage(i).then(() => {
                this.renderPdf({
                  width: this.canvas[i - 1].width,
                  height: this.canvas[i - 1].height
                });
              });
            }
            setTimeout(() => {
              this.renderFabric();
              this.canvasEvents();
            }, 1000);
          });
        });
    },
    // 设置pdf宽高,缩放比例,渲染pdf
    renderPage(num) {
      // console.log('this.canvas', this.canvas[num], num);
      return this.pdfDoc.getPage(num).then((page) => {
        const viewport = page.getViewport({ scale: this.scale }); // 设置视口大小
        this.canvas[num - 1].height = viewport.height;
        this.canvas[num - 1].width = viewport.width;
        // Render PDF page into canvas context
        const renderContext = {
          canvasContext: this.ctx[num - 1],
          viewport: viewport
        };
        page.render(renderContext);
      });
    },
    // 设置绘图区域宽高
    renderPdf(data) {
      this.whDatas = data;
    },
    // 生成绘图区域
    renderFabric() {
      // 1. 拿到全部的canvas-layout
      const canvasLayoutDom = document.querySelectorAll('.canvas-layout');
      // 2. 循环遍历
      canvasLayoutDom.forEach((item) => {
        this.canvasLayoutTopList.push({ obj: item, top: item.offsetTop });
        // 3. 设置宽高和居中
        item.style.width = this.whDatas.width + 'px';
        item.style.height = this.whDatas.height + 'px';
        item.style.margin = '0 auto 18px';
        item.style.boxShadow = '4px 4px 4px #e9e9e9';

        // 4. 拿到盖章canvas
        const canvasEle = item.querySelector('.ele-canvas');
        // 5. 拿到pdf的canvas
        const pCenter = item.querySelector('.the-canvas');
        // 6. 设置盖章canvas的宽高
        canvasEle.width = pCenter.clientWidth;
        canvasEle.height = this.whDatas.height;
        // 7. 创建fabric对象并存储
        this.canvasEle.push(new fabric.Canvas(canvasEle));
        // 8. 设置盖章canvas的样式
        const container = item.querySelector('.canvas-container');
        container.style.position = 'absolute';
        container.style.left = '50%';
        container.style.transform = 'translateX(-50%)';
        container.style.top = '0px';
      });

      // 现形
      this.isFirst = false;
      this.isShowPdf = true;
      this.outViewDom = document.querySelector('.out-view');
      // 开启监听窗口滚动
      this.outViewScroll();
    },
    // 开启监听窗口滚动
    outViewScroll() {
      this.outViewDom.addEventListener('scroll', this.outViewRun);
    },
    // 关闭监听窗口滚动
    outViewScrollClose() {
      this.outViewDom.removeEventListener('scroll', this.outViewRun);
    },
    // 窗口滚动
    outViewRun() {
      const scrollTop = this.outViewDom.scrollTop;
      const topList = this.canvasLayoutTopList.map((item) => item.top);
      // 增加一个最大值
      topList.push(Number.MAX_SAFE_INTEGER);
      for (let index = 0; index < topList.length; index++) {
        const element = topList[index];
        if (element <= scrollTop && scrollTop < topList[index + 1]) {
          this.pageNum = index + 1;
          break;
        }
      }
    },
    // scale滑块,重新渲染整个pdf
    sliderChange() {
      this.pageNum = 1;
      this.numPages = 0;
      this.canvasLayoutTopList = [];
      this.canvasEle = [];
      this.ctx = [];
      this.canvas = [];
      this.isShowPdf = false;
      // this.outViewScrollClose();
      this.whDatas = null;
      this.coordinateList = [{ name: '名称', page: '所在页面', left: 'x坐标', top: 'Y坐标' }];
      this.getSignatureJson();
      setTimeout(() => {
        this.numPages = this.pdfDoc.numPages;
        this.$nextTick(() => {
          this.canvas = document.querySelectorAll('.the-canvas');
          this.canvas.forEach((item) => {
            this.ctx.push(item.getContext('2d'));
          });
          // 循环渲染pdf
          for (let i = 1; i <= this.numPages; i++) {
            this.renderPage(i).then(() => {
              this.renderPdf({
                width: this.canvas[i - 1].width,
                height: this.canvas[i - 1].height
              });
            });
          }
          setTimeout(() => {
            this.renderFabric();
            this.canvasEvents();
          }, 1000);
        });
      }, 1000);
    },
    /**
     * 签章相关部分
     */
    // 签章拖拽边界处理,不能将图片拖拽到绘图区域外
    canvasEvents() {
      this.canvasEle.forEach((item) => {
        item.on('object:moving', (e) => {
          const 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
            );
          }
          // console.log('obj.cacheKey',obj.cacheKey);
          const findIndex = this.coordinateList
            .slice(1)
            .findIndex((coord) => coord.cacheKey == obj.cacheKey);
          const keys = ['width', 'height', 'top', 'left', 'angle', 'scaleX', 'scaleY'];
          keys.forEach((item) => {
            this.coordinateList[findIndex + 1][item] = Math.ceil(obj[item] / this.scale);
          });
          this.getSignatureJson();
        });
      });
    },
    // 拖拽结束
    end(e) {
      // 找到当前拖拽到哪一个canvas-layout上
      const currentCanvasLayout = e.originalEvent.target.parentElement.parentElement;
      const findIndex = this.canvasLayoutTopList.findIndex(
        (item) => item.obj == currentCanvasLayout
      );
      if (findIndex == -1) return false;
      // 取整
      const left = e.originalEvent.layerX < 0 ? 0 : Math.ceil(e.originalEvent.layerX / this.scale);
      const top = e.originalEvent.layerY < 0 ? 0 : Math.ceil(e.originalEvent.layerY / this.scale);
      // console.log('e', e, findIndex);
      this.addSeal({
        sealUrl: this.mainImagelist[e.newDraggableIndex].img,
        left,
        top,
        index: e.newDraggableIndex,
        pageNum: findIndex
      });
    },
    // 添加公章
    addSeal({ sealUrl, left, top, index, pageNum }) {
      fabric.Image.fromURL(sealUrl, (oImg) => {
        oImg.set({
          // 距离左边的距离
          left: left,
          // 距离顶部的距离
          top: top,
          // 角度
          // angle: 10,
          // 缩放比例,需要乘以scale
          scaleX: 0.8 * this.scale,
          scaleY: 0.8 * this.scale,
          index,
          // 禁止缩放
          lockScalingX: true,
          lockScalingY: true,
          // 禁止旋转
          lockRotation: true
        });
        this.canvasEle[pageNum].add(oImg);
        // 保存签章信息
        this.saveSignature({ pageNum, index, sealUrl });
      });
      // this.removeActive();
    },
    // 保存签章
    saveSignature({ pageNum, index, sealUrl }) {
      // 1. 拿到当前签章的信息
      let length = 0;
      let pageConfig = this.coordinateList.filter((item) => item.page - 1 == pageNum);
      if (pageConfig) length = pageConfig.length;
      const currentSignInfo = this.canvasEle[pageNum].getObjects()[length];
      // 2. 拼接数据
      const keys = ['width', 'height', 'top', 'left', 'angle', 'scaleX', 'scaleY'];
      const obj = {};
      keys.forEach((item) => {
        obj[item] = Math.ceil(currentSignInfo[item] / this.scale);
      });
      obj.cacheKey = currentSignInfo.cacheKey;
      obj.sealUrl = sealUrl;
      obj.index = index;
      obj.name = `${this.mainImagelist[index].name}${this.coordinateList.length}`;
      obj.page = pageNum + 1;
      this.coordinateList.push(obj);
      this.getSignatureJson();
    },
    // 签章生成json字符串
    getSignatureJson() {
      // 1. 判断是否有签章
      if (this.coordinateList.length <= 1) return (this.shadowInputValue = '');
      // 2. 拿到签章的信息,去除第一条
      const signatureList = this.coordinateList.slice(1);
      // 3. 拼接数据,只要left和top和page
      const keys = ['page', 'left', 'top'];
      const arr = [];
      signatureList.forEach((item) => {
        const obj = {};
        keys.forEach((key) => {
          obj[key] = item[key];
        });
        arr.push(obj);
      });
      // 4. 转成json字符串
      this.shadowInputValue = JSON.stringify(arr);
    },
    /**
     * 操作相关部分
     */
    // 上一页
    prevPage() {
      if (this.pageNum <= 1) return;
      this.pageNum--;
      // 滚动到指定位置
      this.outViewDom.scrollTop = this.canvasLayoutTopList[this.pageNum - 1].top;
    },
    // 下一页
    nextPage() {
      if (this.pageNum >= this.numPages) return;
      this.pageNum++;
      // 滚动到指定位置
      this.outViewDom.scrollTop = this.canvasLayoutTopList[this.pageNum - 1].top;
    },
    // 切换页码
    cutover() {
      this.outViewScrollClose();
      if (this.pageNum < 1) {
        this.pageNum = 1;
      } else if (this.pageNum > this.numPages) {
        this.pageNum = this.numPages;
      }
      // 滚动到指定位置
      this.outViewDom.scrollTop = this.canvasLayoutTopList[this.pageNum - 1].top;
      setTimeout(() => {
        this.outViewScroll();
      }, 500);
    },
    // 删除所有的签章选中状态
    removeActive() {
      this.canvasEle.forEach((item) => {
        item.discardActiveObject().renderAll();
      });
    },
    // 删除签章
    removeSignature() {
      // 1. 判断是否有选中的签章
      const findItem = this.canvasEle.filter((item) => item.getActiveObject());
      // 2. 判断选中签章的个数
      if (findItem.length == 0) return this.$message.error('请选择要删除的签章');
      // 3. 判断选中签章的个数是否大于1
      if (findItem.length > 1) {
        this.removeActive();
        return this.$message.error('只能选择删除一个签章,请重新选择');
      }
      // 4. 拿到选中的签章的cacheKey
      const activeObj = findItem[0].getActiveObject();
      const findIndex = this.coordinateList.findIndex(
        (item) => item.cacheKey == activeObj.cacheKey
      );
      // 5. 删除选中的签章
      findItem[0].remove(activeObj);
      // 6. 删除选中的签章的信息
      this.coordinateList.splice(findIndex, 1);
      this.getSignatureJson();
    },
    // 清空签章
    clearSignature() {
      this.canvasEle.forEach((item) => {
        item.clear();
      });
      this.coordinateList = [{ name: '名称', page: '所在页面', left: 'x坐标', top: 'Y坐标' }];
      this.getSignatureJson();
    },
    // 提交数据
    submitSignature() {
      console.log('this.coordinateList', this.coordinateList);
    }
  }
};
</script>
<style lang="scss" scoped>
.contract-signature-view {
  /*pdf部分*/
  .ele-canvas {
    overflow: hidden;
  }
  .title-operation {
    height: 80px;
    padding: 20px 40px;
    display: flex;
    align-items: center;
    justify-content: space-between;
    .title {
      font-size: 20px;
      font-weight: 600;
    }
    border-bottom: 1px solid #e4e4e4;
  }
  .section-box {
    position: relative;
    display: flex;
    height: calc(100vh - 60px);

    .signature-img {
      width: 240px;
      min-width: 240px;
      background-color: #fff;
      padding: 40px 15px;

      border-right: 1px solid #e4e4e4;
      .info {
        margin-bottom: 38px;
        .name {
          font-size: 18px;
          font-weight: 600;
          color: #000000;
          line-height: 25px;
          margin-bottom: 20px;
        }
        .text {
          font-size: 14px;
          color: #000000;
          line-height: 20px;
        }
      }
      .item {
        padding: 10px;
        border: 1px dashed rgba(0, 0, 0, 0.3);
        &:not(:last-child) {
          margin-bottom: 10px;
        }
        .img {
          vertical-align: middle;
          width: 120px;
          background-repeat: no-repeat;
        }
      }
    }
    .main-layout {
      flex: 1;
      background-color: #f7f8fa;
      position: relative;

      &.is-first {
        .operate-box {
          opacity: 0;
        }
      }
      .operate-box {
        opacity: 1;
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
        height: 40px;
        background-color: #fff;
        border-bottom: 1px solid #e4e4e4;
        display: flex;
        justify-content: center;
        align-items: center;
        .slider-box {
          width: 230px;
          display: flex;
          justify-content: center;
          align-items: center;
          border-left: 1px solid #e4e4e4;
          border-right: 1px solid #e4e4e4;
          .slider {
            width: 120px;
          }
          .scale-value {
            margin-left: 24px;
            font-size: 16px;
            color: #000000;
            line-height: 22px;
          }
        }
        .page-change {
          display: flex;
          align-items: center;
          margin-left: 30px;
          .icon {
            cursor: pointer;
            padding: 0 5px;
            color: #c1c1c1;
          }
          .input-box {
            border: none;
            /deep/ .el-input__inner {
              width: 34px;
              height: 20px;
              border: none;
              padding: 0;
              text-align: center;
              border-bottom: 1px solid #e4e4e4;
            }
          }
          .default-text {
            display: flex;
            line-height: 22px;
            margin-right: 5px;
          }
        }
      }
      .out-view {
        height: calc(100vh - 100px);
        margin: 40px auto;
        overflow-x: auto;
        overflow-y: auto;
        padding-top: 20px;
        text-align: center;
        opacity: 0;
        transition: all 0.5s;
        &.is-show {
          opacity: 1;
        }
        .canvas-layout {
          position: relative;
          text-align: center;
          margin: 0 auto 18px;
        }
      }
      .loading {
        width: 20px;
        height: 20px;
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        z-index: 999;
        /deep/ .el-loading-mask {
          background-color: transparent;
        }
      }
    }
    .position-info {
      width: 355px;
      min-width: 355px;
      border-left: 1px solid #e4e4e4;
      background-color: #fff;
      padding: 14px 15px;
      .title {
        font-size: 14px;
        font-weight: 400;
        color: #000000;
        line-height: 20px;
        padding-bottom: 18px;
      }
      .nav {
        display: flex;
        flex-direction: column;
        .item {
          display: flex;
          justify-content: space-between;
          padding: 10px 0;
          border-bottom: 1px solid #eee;
          &:first-child {
            background-color: #f7f8fa;
          }
          span {
            flex: 1;
            text-align: center;
            font-size: 12px;
            color: #000000;
            line-height: 20px;
          }
        }
      }
    }
  }
}
</style>
相关推荐
weixin_3823952336 分钟前
为小工厂量身打造:本地部署的物料管理系统带缺料计算
前端·制造
__zRainy__42 分钟前
解决pnpm v10+不自动构建
前端·pnpm·工程化
猫猫不是喵喵.1 小时前
Vue3 Props 属性
前端·javascript·vue.js
醉城夜风~2 小时前
CSS元素显示模式(display)
前端·css
AI大模型-小华3 小时前
Codex 三方充值快速入门指南
java·前端·数据库·chatgpt·ai编程·codex·chatgpt pro
做前端的娜娜子5 小时前
同一链接实现 PC Web 与移动 H5 自适应
前端·掘金·金石计划
小帅不太帅5 小时前
架构没变、规模没变,DeepSeek V4 Flash 正式版凭什么暴涨 47 分?
前端·aigc·deepseek
jarvisuni6 小时前
DeepSeekFlash前端依旧拉垮,而且变慢了很多!
前端·javascript·算法
卷福同学7 小时前
AI编程出海第二步:验证关键词能否做站
前端·人工智能·后端