vue2 之 实现pdf电子签章

news2024/11/24 13:10:59

一、前情提要

1. 需求

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

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

2. 借鉴

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

一位大佬写的文章 :地址

3. 优化

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

二、下载

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

1. fabric

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

npm install fabric@5.3.0

2. pdfjs-dist

npm install pdfjs-dist@2.5.207

问题一

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

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

// 安装包
npm install babel-loader @babel/core @babel/preset-env -D

在webpack.config.js中配置

{
  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

npm install vuedraggable@2.24.3

三、代码

1. 准备pdf文件

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

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

2. 大佬的代码

<!-- //?模块说明 =>  合同签章模块 -->
<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. 优化后的代码 

<!-- //?模块说明 =>  合同签章模块 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>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1327857.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

椰油酰胺,预计到2026年将达到5.25亿美元

椰油酰胺&#xff0c;也称为椰油酰胺 DEA 或椰油酰胺 MEA&#xff0c;是从椰子油中提取的脂肪酸酰胺的混合物。它通常用作洗发水、香皂和化妆品等个人护理产品中的乳化剂和发泡剂。近年来&#xff0c;受个人护理产品需求增加以及椰油酰胺在食品和制药等其他行业的广泛使用推动&…

安全、高效的MySQL DDL解决方案

MySQL作为目前应用最广泛的开源关系型数据库&#xff0c;是许多网站、应用和商业产品的主要数据存储。在生产环境&#xff0c;线上数据库常常面临着持续的、不断变化的表结构修改&#xff08;DDL&#xff09;&#xff0c;如增加、更改、删除字段和索引等等。其中一些DDL操作在M…

微服务 Spring Cloud 10,如何追踪微服务调用?服务治理的常见手段

目录 一、服务追踪的作用1、优化系统瓶颈2、优化链路调用3、故障排查4、性能优化5、生成网络拓扑图4、透明传输数据 二、节点管理1、服务调用失败一般有两类原因造成&#xff1a;2、服务调用失败的解决方式&#xff1a;3、服务调用失败的具体解决方式&#xff1a; 三、负载均衡…

电脑怎么重装系统?跟着步骤轻松搞定!

电脑系统随着时间的推移可能会变得迟缓或出现其他问题&#xff0c;而重装系统是解决这些问题的有效方法之一。本文将介绍三种电脑怎么重装系统的方法&#xff0c;帮助您在不同情况下选择适合自己的方案&#xff0c;让电脑焕然一新。 方法1&#xff1a;使用系统自带的恢复选项 …

[SWPUCTF 2021 新生赛]gift_F12

打开环境 题目有提示&#xff08;F12&#xff09;&#xff0c;那就查看一下源代码 直接滑到最后 看提示猜测&#xff0c;flag就在源代码里了 ctrlf查找flag 最后得到flag&#xff0c;改一下形式就可以了

ELFK日志收集

文章目录 第一章:ELK日志收集系统介绍日志收集重要性ELK介绍EFK介绍ELFK介绍ES部署Kibana部署第二章:Logstach日志收集Logstash介绍Logstash安装Logstash Input输入插件Logstash Filter过滤插件Logstash Output输出插件Input fileFilter mutatesplit示例add_field示例remove_…

Flink系列之:Savepoints

Flink系列之&#xff1a;Savepoints 一、Savepoints二、分配算子ID三、Savepoint 状态四、算子五、触发Savepoint六、Savepoint 格式七、触发 Savepoint八、使用 YARN 触发 Savepoint九、使用 Savepoint 停止作业十、从 Savepoint 恢复十一、跳过无法映射的状态恢复十二、Resto…

阿里云大模型数据存储解决方案,为 AI 创新提供推动力

云布道师 随着国内首批大模型产品获批名单问世&#xff0c;百“模”大战悄然开启。在这场百“模”大战中&#xff0c;每一款大模型产品的诞生&#xff0c;都离不开数据的支撑。如何有效存储、管理和处理海量多模态数据集&#xff0c;并提升模型训练、推理的效率&#xff0c;保…

【湖仓一体尝试】MYSQL和HIVE数据联合查询

爬了两天大大小小的一堆坑&#xff0c;今天把一个简单的单机环境的流程走通了&#xff0c;记录一笔。 先来个完工环境照&#xff1a; mysqlhadoophiveflinkicebergtrino 得益于IBM OPENJ9的优化&#xff0c;完全启动后的内存占用&#xff1a; 1&#xff09;执行联合查询后的…

【Java探索之旅】我与Java的初相识(二):程序结构与运行关系和JDK,JRE,JVM的关系

&#x1f3a5; 屿小夏 &#xff1a; 个人主页 &#x1f525;个人专栏 &#xff1a; Java入门到精通 &#x1f304; 莫道桑榆晚&#xff0c;为霞尚满天&#xff01; 文章目录 &#x1f4d1;前言一. 第一个Java程序1.1 main方法1.2 Java的程序结构 二. Java程序的运行三. JDK、JR…

【零基础入门】凸优化1:怎么培养研究能力,从模型+优化开始!

凸优化1 优化问题的形式优化问题类别1&#xff1a;凸函数 和 非凸函数优化问题类别2&#xff1a;带条件 和 无条件优化问题类别3&#xff1a;离散 和 连续优化问题类别4&#xff1a;平滑 和 非平滑如何判断一个目标函数是凸函数&#xff0c;还是非凸函数&#xff1f;怎么设计模…

Exynos4412 移植Linux-6.1(九)移植tiny4412_backlight驱动的过程及问题解决

系列文章目录 Exynos4412 移植Linux-6.1&#xff08;一&#xff09;下载、配置、编译Linux-6.1 Exynos4412 移植Linux-6.1&#xff08;二&#xff09;SD卡驱动——解决无法挂载SD卡的根文件系统 Exynos4412 移植Linux-6.1&#xff08;三&#xff09;SD卡驱动——解决mmc0: Ti…

解决 elementPlus 组件内容显示为英文的问题

解决 elementPlus 组件内容显示为英文的问题 一、问题描述 刚开始用 ElementPlus 发现默认的组件内容都是英文的 二、解决办法 找了找&#xff0c;发现是国际化的问题&#xff0c;默认就是显示英文&#xff0c;如果要显示中文需要配置中文显示。 关于显示中文的官方说明&a…

Windows11系统下如何通过.cab文件更新PL2303串口驱动?

Windows11系统下如何通过.cab文件更新PL2303串口驱动? 首先,在微软官方网站上下载所需版本的.cab文件,具体链接如下: https://www.catalog.update.microsoft.com/Search.aspx?q=Prolific%20USB-to-Serial%20Comm%20Port 如下图所示,进入该网站后,找到自己所需的驱动版…

神经网络可视化新工具:TorchExplorer

TorchExplorer是一个交互式探索神经网络的可视化工具&#xff0c;他的主要功能如下&#xff1a; TorchExplorer是一款创新的人工智能工具&#xff0c;专为使用非常规神经网络架构的研究人员设计。可以在本地或者wandb中生成交互式Vega自定义图表&#xff0c;提供网络结构的模块…

掌握Apache Kylin:工作原理、设置指南及实际应用全解析

&#x1f337;&#x1f341; 博主猫头虎 带您 Go to New World.✨&#x1f341; &#x1f984; 博客首页——猫头虎的博客&#x1f390; &#x1f433;《面试题大全专栏》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33a; &a…

设计模式(4)--对象行为(1)--职责链

1. 意图 使多个对象都有机会处理请求&#xff0c;从而避免请求的发送者和接收者之间的耦合关系。 将这些对象连成一条链&#xff0c;并沿着这条链传递该请求&#xff0c;直到有一个对象处理它为止。 2. 两种角色 抽象处理者(Handler)、具体处理者(Concrete Handler) 3. 优点 …

直播怎么录制视频?轻松提升视频质量!

录制直播视频是保存和分享游戏过程、教程或其他在线活动的好方法。随着直播行业的兴起&#xff0c;许多用户都希望能够录制自己的直播内容以供日后观看或与他人分享。可是直播怎么录制视频呢&#xff1f;本文将详细介绍两种直播录制视频的方法&#xff0c;希望通过具体的步骤讲…

Redis-Day3实战篇-商户查询缓存(缓存的添加和更新, 缓存穿透/雪崩/击穿, 缓存工具封装)

Redis-Day3实战篇-商户查询缓存 什么是缓存添加Redis缓存业务流程项目实现练习 - 给店铺类型查询业务添加缓存 缓存更新策略最佳实践方案案例 - 给查询商铺的缓存添加超时剔除和主动更新 缓存穿透/雪崩/击穿缓存穿透概述项目实现 - 商铺查询缓存 缓存雪崩缓存击穿概述互斥锁逻辑…