当前位置:   article > 正文

vue2 之 实现pdf电子签章_pdfjs-dist+fabric实现pdf电子签章

pdfjs-dist+fabric实现pdf电子签章

一、前情提要

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的东东

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

在webpack.config.js中配置

  1. {
  2. test: /\.js$/,
  3. loader: 'babel-loader',
  4. include: [
  5. resolve('src'),
  6. // 转化pdfjs-dist,之所以分开写,是因为pdfjs-dist里面有很多es6的语法,但是我们只需要转化pdfjs-dist里面的web文件夹下的js文件
  7. resolve('node_modules/pdfjs-dist/web/pdf_viewer.js'),
  8. resolve('node_modules/pdfjs-dist/build/pdf.js'),
  9. resolve('node_modules/pdfjs-dist/build/pdf.worker.js'),
  10. resolve('node_modules/pdfjs-dist/build/pdf.worker.entry.js') ]
  11. },

问题二 

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

3. vuedraggable

npm install vuedraggable@2.24.3

三、代码

1. 准备pdf文件

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

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

2. 大佬的代码

  1. <!-- //?模块说明 => 合同签章模块 -->
  2. <template>
  3. <div id="elesign" class="elesign">
  4. <el-row>
  5. <el-col :span="4" style="margin-top: 1%">
  6. <div class="left-title">我的印章</div>
  7. <draggable
  8. v-model="mainImagelist"
  9. :group="{ name: 'itext', pull: 'clone' }"
  10. :sort="false"
  11. @end="end"
  12. >
  13. <transition-group type="transition">
  14. <li v-for="item in mainImagelist" :key="item" class="item" style="text-align: center">
  15. <img :src="item" width="100%;" height="100%" class="imgstyle" />
  16. </li>
  17. </transition-group>
  18. </draggable>
  19. </el-col>
  20. <el-col :span="16" style="text-align: center" class="pCenter">
  21. <div class="page">
  22. <!-- <el-button class="btn-outline-dark" @click="zoomIn">-</el-button>
  23. <span style="color: red">{{ (percentage * 100).toFixed(0) + '%' }}</span>
  24. <el-button class="btn-outline-dark" @click="zoomOut">+</el-button> -->
  25. <el-button class="btn-outline-dark" @click="prevPage">上一页</el-button>
  26. <el-button class="btn-outline-dark" @click="nextPage">下一页</el-button>
  27. <el-button class="btn-outline-dark">{{ pageNum }}/{{ numPages }}页</el-button>
  28. <el-input-number
  29. style="margin: 0 5px; border-radius: 5px"
  30. class="btn-outline-dark"
  31. v-model="pageNum"
  32. :min="1"
  33. :max="numPages"
  34. label="输入页码"
  35. ></el-input-number>
  36. <el-button class="btn-outline-dark" @click="cutover">跳转</el-button>
  37. </div>
  38. <canvas id="the-canvas" />
  39. <!-- 盖章部分 -->
  40. <canvas id="ele-canvas"></canvas>
  41. <div class="ele-control" style="margin-bottom: 2%">
  42. <el-button class="btn-outline-dark" @click="removeSignature">删除签章</el-button>
  43. <el-button class="btn-outline-dark" @click="clearSignature">清除所有签章</el-button>
  44. <el-button class="btn-outline-dark" @click="submitSignature">提交所有签章信息</el-button>
  45. </div>
  46. </el-col>
  47. <el-col :span="4" style="margin-top: 1%">
  48. <div class="left-title">任务信息</div>
  49. <div style="text-align: center">
  50. <div>
  51. <div class="right-item">
  52. <div class="right-item-title">文件主题</div>
  53. <div class="detail-item-desc">{{ taskInfo.title }}</div>
  54. </div>
  55. <div class="right-item">
  56. <div class="right-item-title">发起方</div>
  57. <div class="detail-item-desc">{{ taskInfo.uname }}</div>
  58. </div>
  59. <div class="right-item">
  60. <div class="right-item-title">截止时间</div>
  61. <div class="detail-item-desc">{{ taskInfo.endtime }}</div>
  62. </div>
  63. </div>
  64. </div>
  65. </el-col>
  66. </el-row>
  67. </div>
  68. </template>
  69. <script>
  70. import draggable from 'vuedraggable';
  71. import { fabric } from 'fabric';
  72. import workerSrc from 'pdfjs-dist/es5/build/pdf.worker.entry';
  73. import * as pdfjsViewer from 'pdfjs-dist/web/pdf_viewer';
  74. const pdfjsLib = require('pdfjs-dist/es5/build/pdf.js');
  75. pdfjsLib.GlobalWorkerOptions.workerSrc = workerSrc;
  76. export default {
  77. components: { draggable },
  78. data() {
  79. return {
  80. // pdf预览
  81. pdfUrl: '',
  82. pdfDoc: null,
  83. numPages: 1,
  84. pageNum: 1,
  85. scale: 2.2,
  86. pageRendering: false,
  87. pageNumPending: null,
  88. sealUrl: '',
  89. signUrl: '',
  90. canvas: null,
  91. ctx: null,
  92. canvasEle: null,
  93. whDatas: null,
  94. mainImagelist: [],
  95. taskInfo: {}
  96. // percentage: 1
  97. };
  98. },
  99. computed: {
  100. hasSigna() {
  101. if (this.canvasEle && this.canvasEle.getObjects()[0]) {
  102. return true;
  103. } else {
  104. return false;
  105. }
  106. }
  107. },
  108. created() {
  109. var that = this;
  110. that.mainImagelist = [require('@/assets/img/projectCenter/sign.png'), require('@/assets/img/projectCenter/seal.png')];
  111. that.taskInfo = { title: '测试盖章', uname: '张三', endtime: '2021-09-01 17:59:59' };
  112. this.setPdfArea();
  113. },
  114. mounted() {
  115. // this.showpdf(this.pdfUrl);
  116. if (!pdfjsLib.getDocument || !pdfjsViewer.PDFViewer) {
  117. // eslint-disable-next-line no-alert
  118. alert('Please build the pdfjs-dist library using\n `gulp dist-install`');
  119. }
  120. },
  121. methods: {
  122. // pdf预览
  123. // zoomIn() {
  124. // console.log('缩小');
  125. // if (this.scale <= 0.5) {
  126. // this.$message.error('已经显示最小比例');
  127. // } else {
  128. // this.scale -= 0.1;
  129. // this.percentage -= 0.1;
  130. // this.renderPage(this.pageNum);
  131. // this.renderFabric();
  132. // }
  133. // },
  134. // zoomOut() {
  135. // console.log('放大');
  136. // if (this.scale >= 2.2) {
  137. // this.$message.error('已经显示最大比例');
  138. // } else {
  139. // this.scale += 0.1;
  140. // this.percentage += 0.1;
  141. // this.renderPage(this.pageNum);
  142. // this.renderFabric();
  143. // }
  144. // },
  145. renderPage(num) {
  146. let _this = this;
  147. this.pageRendering = true;
  148. return this.pdfDoc.getPage(num).then((page) => {
  149. let viewport = page.getViewport({ scale: _this.scale }); // 设置视口大小
  150. _this.canvas.height = viewport.height;
  151. _this.canvas.width = viewport.width;
  152. // Render PDF page into canvas context
  153. let renderContext = {
  154. canvasContext: _this.ctx,
  155. viewport: viewport
  156. };
  157. let renderTask = page.render(renderContext);
  158. // Wait for rendering to finish
  159. renderTask.promise.then(() => {
  160. _this.pageRendering = false;
  161. if (_this.pageNumPending !== null) {
  162. // New page rendering is pending
  163. this.renderPage(_this.pageNumPending);
  164. _this.pageNumPending = null;
  165. }
  166. });
  167. });
  168. },
  169. queueRenderPage(num) {
  170. if (this.pageRendering) {
  171. this.pageNumPending = num;
  172. } else {
  173. this.renderPage(num);
  174. }
  175. },
  176. prevPage() {
  177. this.confirmSignature();
  178. if (this.pageNum <= 1) {
  179. return;
  180. }
  181. this.pageNum--;
  182. },
  183. nextPage() {
  184. this.confirmSignature();
  185. if (this.pageNum >= this.numPages) {
  186. return;
  187. }
  188. this.pageNum++;
  189. },
  190. cutover() {
  191. this.confirmSignature();
  192. },
  193. // 渲染pdf,到时还会盖章信息,在渲染时,同时显示出来,不应该在切换页码时才显示印章信息
  194. showpdf(pdfUrl) {
  195. let caches = JSON.parse(localStorage.getItem('signs')); // 获取缓存字符串后转换为对象
  196. // console.log(caches);
  197. if (caches != null) {
  198. let datas = caches[this.pageNum];
  199. if (datas != null && datas != undefined) {
  200. for (let index in datas) {
  201. this.addSeal(
  202. datas[index].sealUrl,
  203. datas[index].left,
  204. datas[index].top,
  205. datas[index].index
  206. );
  207. }
  208. }
  209. }
  210. this.canvas = document.getElementById('the-canvas');
  211. this.ctx = this.canvas.getContext('2d');
  212. pdfjsLib
  213. .getDocument({ url: pdfUrl, rangeChunkSize: 65536, disableAutoFetch: false })
  214. .promise.then((pdfDoc_) => {
  215. this.pdfDoc = pdfDoc_;
  216. this.numPages = this.pdfDoc.numPages;
  217. this.renderPage(this.pageNum).then(() => {
  218. this.renderPdf({
  219. width: this.canvas.width,
  220. height: this.canvas.height
  221. });
  222. });
  223. this.commonSign(this.pageNum, true);
  224. });
  225. },
  226. /**
  227. * 盖章部分开始
  228. */
  229. // 设置绘图区域宽高
  230. renderPdf(data) {
  231. this.whDatas = data;
  232. // document.querySelector("#elesign").style.width = data.width + "px";
  233. },
  234. // 生成绘图区域
  235. renderFabric() {
  236. let canvaEle = document.querySelector('#ele-canvas');
  237. let pCenter = document.querySelector('.pCenter');
  238. canvaEle.width = pCenter.clientWidth;
  239. // canvaEle.height = (this.whDatas.height)*(this.scale);
  240. canvaEle.height = this.whDatas.height;
  241. this.canvasEle = new fabric.Canvas(canvaEle);
  242. let container = document.querySelector('.canvas-container');
  243. container.style.position = 'absolute';
  244. container.style.top = '50px';
  245. // container.style.left = "30%";
  246. },
  247. // 相关事件操作哟
  248. canvasEvents() {
  249. // 拖拽边界 不能将图片拖拽到绘图区域外
  250. this.canvasEle.on('object:moving', function (e) {
  251. var obj = e.target;
  252. // if object is too big ignore
  253. if (obj.currentHeight > obj.canvas.height || obj.currentWidth > obj.canvas.width) {
  254. return;
  255. }
  256. obj.setCoords();
  257. // top-left corner
  258. if (obj.getBoundingRect().top < 0 || obj.getBoundingRect().left < 0) {
  259. obj.top = Math.max(obj.top, obj.top - obj.getBoundingRect().top);
  260. obj.left = Math.max(obj.left, obj.left - obj.getBoundingRect().left);
  261. }
  262. // bot-right corner
  263. if (
  264. obj.getBoundingRect().top + obj.getBoundingRect().height > obj.canvas.height ||
  265. obj.getBoundingRect().left + obj.getBoundingRect().width > obj.canvas.width
  266. ) {
  267. obj.top = Math.min(
  268. obj.top,
  269. obj.canvas.height - obj.getBoundingRect().height + obj.top - obj.getBoundingRect().top
  270. );
  271. obj.left = Math.min(
  272. obj.left,
  273. obj.canvas.width - obj.getBoundingRect().width + obj.left - obj.getBoundingRect().left
  274. );
  275. }
  276. });
  277. },
  278. // 添加公章
  279. addSeal(sealUrl, left, top, index) {
  280. fabric.Image.fromURL(sealUrl, (oImg) => {
  281. oImg.set({
  282. left: left,
  283. top: top,
  284. // angle: 10,
  285. scaleX: 0.8,
  286. scaleY: 0.8,
  287. index: index
  288. });
  289. // oImg.scale(0.5); //图片缩小一
  290. this.canvasEle.add(oImg);
  291. });
  292. },
  293. // 删除签章
  294. removeSignature() {
  295. this.canvasEle.remove(this.canvasEle.getActiveObject());
  296. },
  297. // 翻页展示盖章信息
  298. commonSign(pageNum, isFirst = false) {
  299. if (isFirst == false) this.canvasEle.remove(this.canvasEle.clear()); // 清空页面所有签章
  300. let caches = JSON.parse(localStorage.getItem('signs')); // 获取缓存字符串后转换为对象
  301. // console.log(caches);
  302. if (caches == null) return false;
  303. let datas = caches[this.pageNum];
  304. if (datas != null && datas != undefined) {
  305. for (let index in datas) {
  306. this.addSeal(
  307. datas[index].sealUrl,
  308. datas[index].left,
  309. datas[index].top,
  310. datas[index].index
  311. );
  312. }
  313. }
  314. },
  315. // 确认签章位置并保存到缓存
  316. confirmSignature() {
  317. let data = this.canvasEle.getObjects(); // 获取当前页面内的所有签章信息
  318. let caches = JSON.parse(localStorage.getItem('signs')); // 获取缓存字符串后转换为对象
  319. let signDatas = {}; // 存储当前页的所有签章信息
  320. let i = 0;
  321. // let sealUrl = '';
  322. for (var val of data) {
  323. signDatas[i] = {
  324. width: val.width,
  325. height: val.height,
  326. top: val.top,
  327. left: val.left,
  328. angle: val.angle,
  329. translateX: val.translateX,
  330. translateY: val.translateY,
  331. scaleX: val.scaleX,
  332. scaleY: val.scaleY,
  333. pageNum: this.pageNum,
  334. sealUrl: this.mainImagelist[val.index],
  335. index: val.index
  336. };
  337. i++;
  338. }
  339. if (caches == null) {
  340. caches = {};
  341. caches[this.pageNum] = signDatas;
  342. } else {
  343. caches[this.pageNum] = signDatas;
  344. }
  345. localStorage.setItem('signs', JSON.stringify(caches)); // 对象转字符串后存储到缓存
  346. },
  347. // 提交数据
  348. submitSignature() {
  349. this.confirmSignature();
  350. // let caches = localStorage.getItem('signs');
  351. // console.log(JSON.parse(caches));
  352. return false;
  353. },
  354. // 清空数据
  355. clearSignature() {
  356. this.canvasEle.remove(this.canvasEle.clear()); // 清空页面所有签章
  357. localStorage.removeItem('signs'); // 清除缓存
  358. },
  359. end(e) {
  360. this.addSeal(
  361. this.mainImagelist[e.newDraggableIndex],
  362. e.originalEvent.layerX,
  363. e.originalEvent.layerY,
  364. e.newDraggableIndex
  365. );
  366. },
  367. // 设置PDF预览区域高度
  368. setPdfArea() {
  369. this.pdfUrl = './static/text.pdf';
  370. // this.pdfurl = res.data.data.pdfurl;
  371. this.$nextTick(() => {
  372. this.showpdf(this.pdfUrl); // 接口返回的应该还有盖章信息,不只是pdf
  373. });
  374. }
  375. },
  376. watch: {
  377. whDatas: {
  378. handler() {
  379. const loading = this.$loading({
  380. lock: true,
  381. text: 'Loading',
  382. spinner: 'el-icon-loading',
  383. background: 'rgba(0, 0, 0, 0.7)'
  384. });
  385. if (this.whDatas) {
  386. // console.log(this.whDatas);
  387. loading.close();
  388. this.renderFabric();
  389. this.canvasEvents();
  390. let eleCanvas = document.querySelector('#ele-canvas');
  391. eleCanvas.style = 'border:1px solid #5ea6ef;margin-top: 10px;';
  392. }
  393. }
  394. },
  395. pageNum: function () {
  396. this.commonSign(this.pageNum);
  397. this.queueRenderPage(this.pageNum);
  398. }
  399. }
  400. };
  401. </script>
  402. <style lang="scss" scoped>
  403. /*pdf部分*/
  404. #the-canvas {
  405. margin-top: 10px;
  406. }
  407. html:fullscreen {
  408. background: white;
  409. }
  410. .elesign {
  411. display: flex;
  412. flex: 1;
  413. flex-direction: column;
  414. position: relative;
  415. /* padding-left: 180px; */
  416. margin: auto;
  417. /* width:600px; */
  418. }
  419. .page {
  420. text-align: center;
  421. margin: 0 auto;
  422. margin-top: 1%;
  423. }
  424. #ele-canvas {
  425. /* border: 1px solid #5ea6ef; */
  426. overflow: hidden;
  427. }
  428. .ele-control {
  429. text-align: center;
  430. margin-top: 3%;
  431. }
  432. #page-input {
  433. width: 7%;
  434. }
  435. @keyframes ani-demo-spin {
  436. from {
  437. transform: rotate(0deg);
  438. }
  439. 50% {
  440. transform: rotate(180deg);
  441. }
  442. to {
  443. transform: rotate(360deg);
  444. }
  445. }
  446. /* .loadingclass{
  447. position: absolute;
  448. top:30%;
  449. left:49%;
  450. z-index: 99;
  451. } */
  452. .left {
  453. position: absolute;
  454. top: 42px;
  455. left: -5px;
  456. padding: 5px 5px;
  457. /*border: 1px solid #eee;*/
  458. /*border-radius: 4px;*/
  459. }
  460. .left-title {
  461. text-align: center;
  462. padding-bottom: 10px;
  463. border-bottom: 1px solid #eee;
  464. }
  465. li {
  466. list-style-type: none;
  467. padding: 10px;
  468. }
  469. .imgstyle {
  470. vertical-align: middle;
  471. width: 130px;
  472. border: solid 1px #e8eef2;
  473. background-image: url('~@/assets/img/projectCenter/tuo.png');
  474. background-repeat: no-repeat;
  475. }
  476. .right {
  477. position: absolute;
  478. top: 7px;
  479. right: -177px;
  480. margin-top: 34px;
  481. padding-top: 10px;
  482. padding-bottom: 20px;
  483. width: 152px;
  484. /*border: 1px solid #eee;*/
  485. /*border-radius: 4px;*/
  486. }
  487. .right-item {
  488. margin-bottom: 15px;
  489. margin-left: 10px;
  490. }
  491. .right-item-title {
  492. color: #777;
  493. height: 20px;
  494. line-height: 20px;
  495. font-size: 12px;
  496. font-weight: 400;
  497. text-align: left !important;
  498. }
  499. .detail-item-desc {
  500. color: #333;
  501. line-height: 20px;
  502. width: 100%;
  503. font-size: 12px;
  504. display: inline-block;
  505. text-align: left;
  506. }
  507. .btn-outline-dark {
  508. color: #0f1531;
  509. background-color: transparent;
  510. background-image: none;
  511. border: 1px solid #3e4b5b;
  512. }
  513. .btn-outline-dark:hover {
  514. color: #fff;
  515. background-color: #3e4b5b;
  516. border-color: #3e4b5b;
  517. }
  518. </style>

3. 优化后的代码 

  1. <!-- //?模块说明 => 合同签章模块 addToTab-->
  2. <template>
  3. <div class="contract-signature-view">
  4. <div class="title-operation">
  5. <h2 class="title">合同签章</h2>
  6. <div class="operation">
  7. <el-button type="danger" @click="removeSignature">删除签章</el-button>
  8. <el-button type="danger" @click="clearSignature">清空签章</el-button>
  9. <el-button type="primary" @click="submitSignature">提交签章</el-button>
  10. </div>
  11. </div>
  12. <div class="section-box">
  13. <!-- 签章图片 -->
  14. <aside class="signature-img">
  15. <div class="info">
  16. <h3 class="name">印章</h3>
  17. <p class="text">将示例印章标识拖到文件相应区域即可获取签章位置</p>
  18. </div>
  19. <!-- 拖拽 -->
  20. <draggable
  21. v-model="mainImagelist"
  22. :group="{ name: 'itext', pull: 'clone' }"
  23. :sort="false"
  24. @end="end"
  25. >
  26. <transition-group type="transition">
  27. <li
  28. v-for="item in mainImagelist"
  29. :key="item.img"
  30. class="item"
  31. style="text-align: center"
  32. >
  33. <img :src="item.img" width="100%;" height="100%" class="img" />
  34. </li>
  35. </transition-group>
  36. </draggable>
  37. </aside>
  38. <!-- 主体区域 -->
  39. <section class="main-layout" :class="{ 'is-first': isFirst }">
  40. <!-- 操作 -->
  41. <div class="operate-box">
  42. <div class="slider-box">
  43. <el-slider
  44. class="slider"
  45. v-model="scale"
  46. :min="0.5"
  47. :max="2"
  48. :step="0.1"
  49. :show-tooltip="false"
  50. @change="sliderChange"
  51. />
  52. <span class="scale-value">{{ (scale * 100).toFixed(0) + '%' }}</span>
  53. </div>
  54. <div class="page-change">
  55. <i class="icon el-icon-arrow-left" @click="prevPage" />
  56. <!-- :min="1" -->
  57. <el-input
  58. class="input-box"
  59. v-model.number="pageNum"
  60. :max="defaultNumPages"
  61. @change="cutover"
  62. />
  63. <span class="default-text">/{{ defaultNumPages }}</span>
  64. <i class="icon el-icon-arrow-right" @click="nextPage" />
  65. </div>
  66. </div>
  67. <!-- 画图 -->
  68. <div class="out-view" :class="{ 'is-show': isShowPdf }">
  69. <div class="canvas-layout" v-for="item in numPages" :key="item">
  70. <!-- pdf部分 -->
  71. <canvas class="the-canvas" />
  72. <!-- 盖章部分 -->
  73. <canvas class="ele-canvas"></canvas>
  74. </div>
  75. </div>
  76. <i class="loading" v-loading="!isShowPdf" />
  77. </section>
  78. <!-- 位置信息 -->
  79. <div class="position-info">
  80. <h3 class="title">位置信息</h3>
  81. <ul class="nav">
  82. <li class="item" v-for="(item, index) in coordinateList" :key="index">
  83. <span>{{ item.name }}</span>
  84. <span>{{ item.page }}</span>
  85. <span>{{ item.left }}</span>
  86. <span>{{ item.top }}</span>
  87. </li>
  88. </ul>
  89. </div>
  90. </div>
  91. </div>
  92. </template>
  93. <script>
  94. // 拖拽插件
  95. import draggable from 'vuedraggable';
  96. // pdf插件
  97. import { fabric } from 'fabric';
  98. import workerSrc from 'pdfjs-dist/es5/build/pdf.worker.entry';
  99. const pdfjsLib = require('pdfjs-dist/es5/build/pdf.js');
  100. pdfjsLib.GlobalWorkerOptions.workerSrc = workerSrc;
  101. export default {
  102. components: { draggable },
  103. data() {
  104. return {
  105. // pdf地址
  106. pdfUrl: '',
  107. // 左侧签章列表
  108. mainImagelist: [],
  109. // 右侧坐标数据
  110. coordinateList: [{ name: '名称', page: '所在页面', left: 'x坐标', top: 'Y坐标' }],
  111. // 总页数
  112. numPages: 1,
  113. defaultNumPages: 1,
  114. // 当前页
  115. pageNum: 1,
  116. // 缩放比例
  117. scale: 1,
  118. // pdf是否显示
  119. isFirst: true,
  120. isShowPdf: false,
  121. // pdf最外层的out-view
  122. outViewDom: null,
  123. // 各页pdf的canvas-layout
  124. canvasLayoutTopList: [],
  125. // 用来签章的canvas数组
  126. canvasEle: [],
  127. // 绘图区域的宽高
  128. whDatas: null,
  129. // pdf渲染的canvas数组
  130. canvas: [],
  131. // pdf渲染的canvas的ctx数组
  132. ctx: [],
  133. // pdf渲染的canvas的宽高
  134. pdfDoc: null,
  135. // 隐藏的input,用来提交数据
  136. shadowInputValue: ''
  137. };
  138. },
  139. created() {
  140. this.mainImagelist = [
  141. { name: '印章', img: require('@/assets/img/projectCenter/contract-sign-img.png') }
  142. // { name: '印章', img: require('./sign.png') },
  143. // { name: '红章', img: require('@/assets/img/projectCenter/seal.png') }
  144. ];
  145. this.setPdfArea();
  146. },
  147. mounted() {},
  148. methods: {
  149. /**
  150. * pdf相关部分
  151. */
  152. // 设置PDF地址
  153. setPdfArea() {
  154. // // 1. 获取地址栏
  155. // const urlString = window.location.href;
  156. // // 2. 截取地址栏
  157. // const pdfStr = urlString.split('?')[1];
  158. // // 3. 截取pdf地址并解码
  159. // this.pdfUrl = decodeURIComponent(pdfStr.split('=')[1]);
  160. this.pdfUrl = './static/text.pdf';
  161. this.$nextTick(() => {
  162. this.showpdf(this.pdfUrl); // 接口返回的应该还有盖章信息,不只是pdf
  163. });
  164. },
  165. // 解析pdf
  166. showpdf(pdfUrl) {
  167. pdfjsLib
  168. .getDocument({ url: pdfUrl, rangeChunkSize: 65536, disableAutoFetch: false })
  169. .promise.then((pdfDoc_) => {
  170. this.pdfDoc = pdfDoc_;
  171. this.numPages = this.pdfDoc.numPages;
  172. this.defaultNumPages = this.pdfDoc.numPages;
  173. this.$nextTick(() => {
  174. this.canvas = document.querySelectorAll('.the-canvas');
  175. this.canvas.forEach((item) => {
  176. this.ctx.push(item.getContext('2d'));
  177. });
  178. // 循环渲染pdf
  179. for (let i = 1; i <= this.numPages; i++) {
  180. this.renderPage(i).then(() => {
  181. this.renderPdf({
  182. width: this.canvas[i - 1].width,
  183. height: this.canvas[i - 1].height
  184. });
  185. });
  186. }
  187. setTimeout(() => {
  188. this.renderFabric();
  189. this.canvasEvents();
  190. }, 1000);
  191. });
  192. });
  193. },
  194. // 设置pdf宽高,缩放比例,渲染pdf
  195. renderPage(num) {
  196. // console.log('this.canvas', this.canvas[num], num);
  197. return this.pdfDoc.getPage(num).then((page) => {
  198. const viewport = page.getViewport({ scale: this.scale }); // 设置视口大小
  199. this.canvas[num - 1].height = viewport.height;
  200. this.canvas[num - 1].width = viewport.width;
  201. // Render PDF page into canvas context
  202. const renderContext = {
  203. canvasContext: this.ctx[num - 1],
  204. viewport: viewport
  205. };
  206. page.render(renderContext);
  207. });
  208. },
  209. // 设置绘图区域宽高
  210. renderPdf(data) {
  211. this.whDatas = data;
  212. },
  213. // 生成绘图区域
  214. renderFabric() {
  215. // 1. 拿到全部的canvas-layout
  216. const canvasLayoutDom = document.querySelectorAll('.canvas-layout');
  217. // 2. 循环遍历
  218. canvasLayoutDom.forEach((item) => {
  219. this.canvasLayoutTopList.push({ obj: item, top: item.offsetTop });
  220. // 3. 设置宽高和居中
  221. item.style.width = this.whDatas.width + 'px';
  222. item.style.height = this.whDatas.height + 'px';
  223. item.style.margin = '0 auto 18px';
  224. item.style.boxShadow = '4px 4px 4px #e9e9e9';
  225. // 4. 拿到盖章canvas
  226. const canvasEle = item.querySelector('.ele-canvas');
  227. // 5. 拿到pdf的canvas
  228. const pCenter = item.querySelector('.the-canvas');
  229. // 6. 设置盖章canvas的宽高
  230. canvasEle.width = pCenter.clientWidth;
  231. canvasEle.height = this.whDatas.height;
  232. // 7. 创建fabric对象并存储
  233. this.canvasEle.push(new fabric.Canvas(canvasEle));
  234. // 8. 设置盖章canvas的样式
  235. const container = item.querySelector('.canvas-container');
  236. container.style.position = 'absolute';
  237. container.style.left = '50%';
  238. container.style.transform = 'translateX(-50%)';
  239. container.style.top = '0px';
  240. });
  241. // 现形
  242. this.isFirst = false;
  243. this.isShowPdf = true;
  244. this.outViewDom = document.querySelector('.out-view');
  245. // 开启监听窗口滚动
  246. this.outViewScroll();
  247. },
  248. // 开启监听窗口滚动
  249. outViewScroll() {
  250. this.outViewDom.addEventListener('scroll', this.outViewRun);
  251. },
  252. // 关闭监听窗口滚动
  253. outViewScrollClose() {
  254. this.outViewDom.removeEventListener('scroll', this.outViewRun);
  255. },
  256. // 窗口滚动
  257. outViewRun() {
  258. const scrollTop = this.outViewDom.scrollTop;
  259. const topList = this.canvasLayoutTopList.map((item) => item.top);
  260. // 增加一个最大值
  261. topList.push(Number.MAX_SAFE_INTEGER);
  262. for (let index = 0; index < topList.length; index++) {
  263. const element = topList[index];
  264. if (element <= scrollTop && scrollTop < topList[index + 1]) {
  265. this.pageNum = index + 1;
  266. break;
  267. }
  268. }
  269. },
  270. // scale滑块,重新渲染整个pdf
  271. sliderChange() {
  272. this.pageNum = 1;
  273. this.numPages = 0;
  274. this.canvasLayoutTopList = [];
  275. this.canvasEle = [];
  276. this.ctx = [];
  277. this.canvas = [];
  278. this.isShowPdf = false;
  279. // this.outViewScrollClose();
  280. this.whDatas = null;
  281. this.coordinateList = [{ name: '名称', page: '所在页面', left: 'x坐标', top: 'Y坐标' }];
  282. this.getSignatureJson();
  283. setTimeout(() => {
  284. this.numPages = this.pdfDoc.numPages;
  285. this.$nextTick(() => {
  286. this.canvas = document.querySelectorAll('.the-canvas');
  287. this.canvas.forEach((item) => {
  288. this.ctx.push(item.getContext('2d'));
  289. });
  290. // 循环渲染pdf
  291. for (let i = 1; i <= this.numPages; i++) {
  292. this.renderPage(i).then(() => {
  293. this.renderPdf({
  294. width: this.canvas[i - 1].width,
  295. height: this.canvas[i - 1].height
  296. });
  297. });
  298. }
  299. setTimeout(() => {
  300. this.renderFabric();
  301. this.canvasEvents();
  302. }, 1000);
  303. });
  304. }, 1000);
  305. },
  306. /**
  307. * 签章相关部分
  308. */
  309. // 签章拖拽边界处理,不能将图片拖拽到绘图区域外
  310. canvasEvents() {
  311. this.canvasEle.forEach((item) => {
  312. item.on('object:moving', (e) => {
  313. const obj = e.target;
  314. // if object is too big ignore
  315. if (obj.currentHeight > obj.canvas.height || obj.currentWidth > obj.canvas.width) {
  316. return;
  317. }
  318. obj.setCoords();
  319. // top-left corner
  320. if (obj.getBoundingRect().top < 0 || obj.getBoundingRect().left < 0) {
  321. obj.top = Math.max(obj.top, obj.top - obj.getBoundingRect().top);
  322. obj.left = Math.max(obj.left, obj.left - obj.getBoundingRect().left);
  323. }
  324. // bot-right corner
  325. if (
  326. obj.getBoundingRect().top + obj.getBoundingRect().height > obj.canvas.height ||
  327. obj.getBoundingRect().left + obj.getBoundingRect().width > obj.canvas.width
  328. ) {
  329. obj.top = Math.min(
  330. obj.top,
  331. obj.canvas.height - obj.getBoundingRect().height + obj.top - obj.getBoundingRect().top
  332. );
  333. obj.left = Math.min(
  334. obj.left,
  335. obj.canvas.width - obj.getBoundingRect().width + obj.left - obj.getBoundingRect().left
  336. );
  337. }
  338. // console.log('obj.cacheKey',obj.cacheKey);
  339. const findIndex = this.coordinateList
  340. .slice(1)
  341. .findIndex((coord) => coord.cacheKey == obj.cacheKey);
  342. const keys = ['width', 'height', 'top', 'left', 'angle', 'scaleX', 'scaleY'];
  343. keys.forEach((item) => {
  344. this.coordinateList[findIndex + 1][item] = Math.ceil(obj[item] / this.scale);
  345. });
  346. this.getSignatureJson();
  347. });
  348. });
  349. },
  350. // 拖拽结束
  351. end(e) {
  352. // 找到当前拖拽到哪一个canvas-layout上
  353. const currentCanvasLayout = e.originalEvent.target.parentElement.parentElement;
  354. const findIndex = this.canvasLayoutTopList.findIndex(
  355. (item) => item.obj == currentCanvasLayout
  356. );
  357. if (findIndex == -1) return false;
  358. // 取整
  359. const left = e.originalEvent.layerX < 0 ? 0 : Math.ceil(e.originalEvent.layerX / this.scale);
  360. const top = e.originalEvent.layerY < 0 ? 0 : Math.ceil(e.originalEvent.layerY / this.scale);
  361. // console.log('e', e, findIndex);
  362. this.addSeal({
  363. sealUrl: this.mainImagelist[e.newDraggableIndex].img,
  364. left,
  365. top,
  366. index: e.newDraggableIndex,
  367. pageNum: findIndex
  368. });
  369. },
  370. // 添加公章
  371. addSeal({ sealUrl, left, top, index, pageNum }) {
  372. fabric.Image.fromURL(sealUrl, (oImg) => {
  373. oImg.set({
  374. // 距离左边的距离
  375. left: left,
  376. // 距离顶部的距离
  377. top: top,
  378. // 角度
  379. // angle: 10,
  380. // 缩放比例,需要乘以scale
  381. scaleX: 0.8 * this.scale,
  382. scaleY: 0.8 * this.scale,
  383. index,
  384. // 禁止缩放
  385. lockScalingX: true,
  386. lockScalingY: true,
  387. // 禁止旋转
  388. lockRotation: true
  389. });
  390. this.canvasEle[pageNum].add(oImg);
  391. // 保存签章信息
  392. this.saveSignature({ pageNum, index, sealUrl });
  393. });
  394. // this.removeActive();
  395. },
  396. // 保存签章
  397. saveSignature({ pageNum, index, sealUrl }) {
  398. // 1. 拿到当前签章的信息
  399. let length = 0;
  400. let pageConfig = this.coordinateList.filter((item) => item.page - 1 == pageNum);
  401. if (pageConfig) length = pageConfig.length;
  402. const currentSignInfo = this.canvasEle[pageNum].getObjects()[length];
  403. // 2. 拼接数据
  404. const keys = ['width', 'height', 'top', 'left', 'angle', 'scaleX', 'scaleY'];
  405. const obj = {};
  406. keys.forEach((item) => {
  407. obj[item] = Math.ceil(currentSignInfo[item] / this.scale);
  408. });
  409. obj.cacheKey = currentSignInfo.cacheKey;
  410. obj.sealUrl = sealUrl;
  411. obj.index = index;
  412. obj.name = `${this.mainImagelist[index].name}${this.coordinateList.length}`;
  413. obj.page = pageNum + 1;
  414. this.coordinateList.push(obj);
  415. this.getSignatureJson();
  416. },
  417. // 签章生成json字符串
  418. getSignatureJson() {
  419. // 1. 判断是否有签章
  420. if (this.coordinateList.length <= 1) return (this.shadowInputValue = '');
  421. // 2. 拿到签章的信息,去除第一条
  422. const signatureList = this.coordinateList.slice(1);
  423. // 3. 拼接数据,只要left和top和page
  424. const keys = ['page', 'left', 'top'];
  425. const arr = [];
  426. signatureList.forEach((item) => {
  427. const obj = {};
  428. keys.forEach((key) => {
  429. obj[key] = item[key];
  430. });
  431. arr.push(obj);
  432. });
  433. // 4. 转成json字符串
  434. this.shadowInputValue = JSON.stringify(arr);
  435. },
  436. /**
  437. * 操作相关部分
  438. */
  439. // 上一页
  440. prevPage() {
  441. if (this.pageNum <= 1) return;
  442. this.pageNum--;
  443. // 滚动到指定位置
  444. this.outViewDom.scrollTop = this.canvasLayoutTopList[this.pageNum - 1].top;
  445. },
  446. // 下一页
  447. nextPage() {
  448. if (this.pageNum >= this.numPages) return;
  449. this.pageNum++;
  450. // 滚动到指定位置
  451. this.outViewDom.scrollTop = this.canvasLayoutTopList[this.pageNum - 1].top;
  452. },
  453. // 切换页码
  454. cutover() {
  455. this.outViewScrollClose();
  456. if (this.pageNum < 1) {
  457. this.pageNum = 1;
  458. } else if (this.pageNum > this.numPages) {
  459. this.pageNum = this.numPages;
  460. }
  461. // 滚动到指定位置
  462. this.outViewDom.scrollTop = this.canvasLayoutTopList[this.pageNum - 1].top;
  463. setTimeout(() => {
  464. this.outViewScroll();
  465. }, 500);
  466. },
  467. // 删除所有的签章选中状态
  468. removeActive() {
  469. this.canvasEle.forEach((item) => {
  470. item.discardActiveObject().renderAll();
  471. });
  472. },
  473. // 删除签章
  474. removeSignature() {
  475. // 1. 判断是否有选中的签章
  476. const findItem = this.canvasEle.filter((item) => item.getActiveObject());
  477. // 2. 判断选中签章的个数
  478. if (findItem.length == 0) return this.$message.error('请选择要删除的签章');
  479. // 3. 判断选中签章的个数是否大于1
  480. if (findItem.length > 1) {
  481. this.removeActive();
  482. return this.$message.error('只能选择删除一个签章,请重新选择');
  483. }
  484. // 4. 拿到选中的签章的cacheKey
  485. const activeObj = findItem[0].getActiveObject();
  486. const findIndex = this.coordinateList.findIndex(
  487. (item) => item.cacheKey == activeObj.cacheKey
  488. );
  489. // 5. 删除选中的签章
  490. findItem[0].remove(activeObj);
  491. // 6. 删除选中的签章的信息
  492. this.coordinateList.splice(findIndex, 1);
  493. this.getSignatureJson();
  494. },
  495. // 清空签章
  496. clearSignature() {
  497. this.canvasEle.forEach((item) => {
  498. item.clear();
  499. });
  500. this.coordinateList = [{ name: '名称', page: '所在页面', left: 'x坐标', top: 'Y坐标' }];
  501. this.getSignatureJson();
  502. },
  503. // 提交数据
  504. submitSignature() {
  505. console.log('this.coordinateList', this.coordinateList);
  506. }
  507. }
  508. };
  509. </script>
  510. <style lang="scss" scoped>
  511. .contract-signature-view {
  512. /*pdf部分*/
  513. .ele-canvas {
  514. overflow: hidden;
  515. }
  516. .title-operation {
  517. height: 80px;
  518. padding: 20px 40px;
  519. display: flex;
  520. align-items: center;
  521. justify-content: space-between;
  522. .title {
  523. font-size: 20px;
  524. font-weight: 600;
  525. }
  526. border-bottom: 1px solid #e4e4e4;
  527. }
  528. .section-box {
  529. position: relative;
  530. display: flex;
  531. height: calc(100vh - 60px);
  532. .signature-img {
  533. width: 240px;
  534. min-width: 240px;
  535. background-color: #fff;
  536. padding: 40px 15px;
  537. border-right: 1px solid #e4e4e4;
  538. .info {
  539. margin-bottom: 38px;
  540. .name {
  541. font-size: 18px;
  542. font-weight: 600;
  543. color: #000000;
  544. line-height: 25px;
  545. margin-bottom: 20px;
  546. }
  547. .text {
  548. font-size: 14px;
  549. color: #000000;
  550. line-height: 20px;
  551. }
  552. }
  553. .item {
  554. padding: 10px;
  555. border: 1px dashed rgba(0, 0, 0, 0.3);
  556. &:not(:last-child) {
  557. margin-bottom: 10px;
  558. }
  559. .img {
  560. vertical-align: middle;
  561. width: 120px;
  562. background-repeat: no-repeat;
  563. }
  564. }
  565. }
  566. .main-layout {
  567. flex: 1;
  568. background-color: #f7f8fa;
  569. position: relative;
  570. &.is-first {
  571. .operate-box {
  572. opacity: 0;
  573. }
  574. }
  575. .operate-box {
  576. opacity: 1;
  577. position: absolute;
  578. top: 0;
  579. left: 0;
  580. width: 100%;
  581. height: 40px;
  582. background-color: #fff;
  583. border-bottom: 1px solid #e4e4e4;
  584. display: flex;
  585. justify-content: center;
  586. align-items: center;
  587. .slider-box {
  588. width: 230px;
  589. display: flex;
  590. justify-content: center;
  591. align-items: center;
  592. border-left: 1px solid #e4e4e4;
  593. border-right: 1px solid #e4e4e4;
  594. .slider {
  595. width: 120px;
  596. }
  597. .scale-value {
  598. margin-left: 24px;
  599. font-size: 16px;
  600. color: #000000;
  601. line-height: 22px;
  602. }
  603. }
  604. .page-change {
  605. display: flex;
  606. align-items: center;
  607. margin-left: 30px;
  608. .icon {
  609. cursor: pointer;
  610. padding: 0 5px;
  611. color: #c1c1c1;
  612. }
  613. .input-box {
  614. border: none;
  615. /deep/ .el-input__inner {
  616. width: 34px;
  617. height: 20px;
  618. border: none;
  619. padding: 0;
  620. text-align: center;
  621. border-bottom: 1px solid #e4e4e4;
  622. }
  623. }
  624. .default-text {
  625. display: flex;
  626. line-height: 22px;
  627. margin-right: 5px;
  628. }
  629. }
  630. }
  631. .out-view {
  632. height: calc(100vh - 100px);
  633. margin: 40px auto;
  634. overflow-x: auto;
  635. overflow-y: auto;
  636. padding-top: 20px;
  637. text-align: center;
  638. opacity: 0;
  639. transition: all 0.5s;
  640. &.is-show {
  641. opacity: 1;
  642. }
  643. .canvas-layout {
  644. position: relative;
  645. text-align: center;
  646. margin: 0 auto 18px;
  647. }
  648. }
  649. .loading {
  650. width: 20px;
  651. height: 20px;
  652. position: absolute;
  653. top: 50%;
  654. left: 50%;
  655. transform: translate(-50%, -50%);
  656. z-index: 999;
  657. /deep/ .el-loading-mask {
  658. background-color: transparent;
  659. }
  660. }
  661. }
  662. .position-info {
  663. width: 355px;
  664. min-width: 355px;
  665. border-left: 1px solid #e4e4e4;
  666. background-color: #fff;
  667. padding: 14px 15px;
  668. .title {
  669. font-size: 14px;
  670. font-weight: 400;
  671. color: #000000;
  672. line-height: 20px;
  673. padding-bottom: 18px;
  674. }
  675. .nav {
  676. display: flex;
  677. flex-direction: column;
  678. .item {
  679. display: flex;
  680. justify-content: space-between;
  681. padding: 10px 0;
  682. border-bottom: 1px solid #eee;
  683. &:first-child {
  684. background-color: #f7f8fa;
  685. }
  686. span {
  687. flex: 1;
  688. text-align: center;
  689. font-size: 12px;
  690. color: #000000;
  691. line-height: 20px;
  692. }
  693. }
  694. }
  695. }
  696. }
  697. }
  698. </style>

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/码创造者/article/detail/880274
推荐阅读
相关标签
  

闽ICP备14008679号