vue使用Fabric和pdfjs完成合同签章及批注
合同签章及批注模块涉及以下核心难点和复杂点,按功能模块归类如下:
效果展示
使用拖拽的方式将签名放入pdf文件,再点击文字批注对文件进行添加批注
难点及功能点描述
一、PDF渲染与交互
多页动态渲染
使用pdfjs-dist库实现PDF分页渲染
动态创建Canvas元素并根据页面尺寸自动调整布局
处理PDF缩放时的全量重新渲染(sliderChange方法)
滚动定位
通过canvasLayoutTopList记录每页位置
滚动事件监听实现精准页码定位(outViewRun方法)
滚动节流处理防止性能问题
二、电子签章系统
图形交互
使用Fabric.js实现可拖拽签章
自定义控制点图标(删除/缩放按钮)
边界检测防止拖出画布(object:moving事件处理)
状态同步
通过coordinateList维护签章坐标/尺寸/角度等状态
实时同步画布操作与数据模型(getSignatureJson方法)
唯一标识符cacheKey管理对象身份
缩放控制
自定义缩放算法(handleScaling方法)
最小缩放限制防止过度缩小
等比缩放保持图形比例
三、文字批注系统
富文本交互
动态创建可编辑文本框(fabric.IText)
字体缩放时自动调整字号(baseFontSize跟踪)
文本框边界检测与自动换行
复合对象管理
每页仅允许单个批注的限制逻辑
文字与签章的状态混合存储(通过name字段区分)
四、文件处理
PDF文件下载
Blob对象处理二进制流
文件名编码解码处理(content-disposition解析)
内存管理(revokeObjectURL)
签章图片上传
自定义上传组件(el-upload封装)
服务端响应处理(二进制流/JSON判断)
Token鉴权处理
五、性能优化
画布渲染优化
局部渲染(canvas.requestRenderAll())
对象缓存(cacheKey机制)
事件委托减少重复绑定
内存管理
Canvas对象销毁逻辑
Blob URL及时释放
六、业务逻辑复杂性
状态验证
签章必填校验(提交前coordinateList检查)
每页签章/批注数量限制
数据转换
坐标系统转换(屏幕坐标与PDF坐标)
服务端数据结构适配(processSignsData方法)
七、浏览器兼容性
Blob API兼容
多浏览器下载兼容处理(Chrome/Firefox/Edge)
大文件下载内存管理
触摸事件支持
移动端拖拽/缩放适配(未完全实现)
前期准备
引入插件
在文件public下增加一个pdfjs文件夹,且再增加一个build文件夹,其中放pdf.js和pdf.worker.js文件
文末放有pdf.js和pdf.worker.js文件源码
// pdf插件
import { fabric } from "fabric";
import * as pdfjsLib from "pdfjs-dist/build/pdf";
import workerSrc from "pdfjs-dist/build/pdf.worker.entry";
import * as pdfjsViewer from "pdfjs-dist/web/pdf_viewer";
主要代码,
这是大佬的地址 vue里面使用pdfjs-dist+fabric实现pdf电子签章!!!
在借鉴了另外一篇文章后进行修改的如下,就是做了点修改(ps:目前印章的位置和坐标保存,使用的得本地缓存,便于调试,后期会保存到接口里面!)
<!-- //?模块说明 => 合同签章模块 addToTab-->
<template><div class="contract-signature-view"><div class="section-box"><!-- 签章图片 --><!-- <aside class="signature-img"><div class="info"><h3 class="name">印章</h3><p class="text">将示例印章标识拖到文件相应区域即可获取签章位置</p></div></aside> --><!-- 主体区域 --><div class="main-layout" :class="{ 'is-first': isFirst }"><!-- 操作 --><div class="title-operation"><div class="operation"><el-buttonv-if="$route.query.formst==31"class="searchbutton"type="primary"@click="optionSignature('addSubSign')"><svg-icon icon-class="check" />呈签</el-button><el-buttonv-elseclass="searchbutton"type="primary"@click="optionSignature('agree')"><svg-icon icon-class="check" />同意</el-button><!-- <el-buttonclass="searchbutton"icon="el-icon-close"@click="optionSignature('return')">回退</el-button> --><!-- <el-button class="searchbutton" @click="download()"><svg-icon icon-class="download" />下载签核文件</el-button> --><el-button class="searchbutton" @click="returnSignature"><svg-icon icon-class="return" />返回</el-button><el-popover placement="bottom" trigger="click"><template #default><div class="signatureimg" v-if="vissign == true"><!-- 拖拽 --><draggablev-model="mainImagelist":group="{ name: 'itext', pull: 'clone' }":sort="false"@end="end"@move="onMove"><transition-group type="transition"><liv-for="item in mainImagelist":key="item.img"class="item"style="text-align: center"><authImg:authSrc="item.img"alt=""class="img"></authImg><!-- <img ref="img" :src="item.img" width="100%;" height="100%" class="img" /> --></li></transition-group></draggable><div class="opera"><span>请拖拽签名至目标位置</span><span><el-buttonclass="delbut"icon="el-icon-delete"@click="deleteSignature">删除</el-button></span></div></div><div v-else class="upimg"><!-- <div style="color: #3f7afa;font-size: 14px;"><svg-icon icon-class="plus" />添加签名</div> --><el-uploadclass="upload-demo"action="#":http-request="customRequest":on-preview="handlePreview":on-remove="handleRemove":before-remove="beforeRemove":limit="1"accept=".png, .jpg":on-exceed="handleExceed"><el-button size="small" class="upload-button"><svg-icon icon-class="plus" /> 添加签名</el-button><divslot="tip"style="font-size: 10px;padding-top: 10px;color:#00000066;">支持上传png图片,大小不超过500kb</div></el-upload></div></template><el-button slot="reference" class="button"><svg-icon icon-class="Esign" /> 电子签名</el-button></el-popover><!-- <el-button class="button" @click="addTextobjectHandle($event)" > <svg-icon icon-class="Esign" /> 电子签名</el-button> --><!-- <el-button type="danger" @click="removeSignature">删除签章</el-button>--><!-- <el-button type="primary" @click="submitSignature">提交签章</el-button> --><el-button class="button" @click="addTextobjectHandle($event)"><svg-icon icon-class="font-size" /> 文字批注</el-button><!-- <el-button type="danger" @click="clearSignature">清空签章</el-button> --></div></div><div class="operate-box"><div class="pageNo-change"><i class="icon el-icon-arrow-left" @click="prevPage" /><el-inputclass="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="container_cont" style="margin-top:1%;position: relative;display: flex;align-items: center;justify-content: center;"><button id="prevpage" style=" width: 120px;height: 30px;background: none;border: 1px solid #b1afaf;border-radius: 5px;font-size: 12px;font-weight: 1000;color: #384240;cursor: pointer;outline: none;margin: 0 0.5%">上一页</button><button id="nextpage" style=" width: 120px;height: 30px;background: none;border: 1px solid #b1afaf;border-radius: 5px;font-size: 12px;font-weight: 1000;color: #384240;cursor: pointer;outline: none; margin: 0 0.5%">下一页</button></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" id="box" /><!-- 签名部分 --><canvas class="ele-canvas" id="canvas"></canvas><!-- <div id="menu" class="menu-x"><div class="menu-li" @click="removeSignature">删除</div></div> --><!-- <canvas class="text-canvas" id="canvas" ></canvas> --></div></div><i class="loading" v-loading="!isShowPdf" /></div><!-- 位置信息 --></div><Dialog:isVisible="isVisible":id="id":scale="scale":title="title"@closeVisible="closeVisible":signlist="signlist"></Dialog></div>
</template><script>
import { getToken } from "@/utils/auth";
import { uploadSignture, removeSignture } from "@/api/create/index";
import userApi from "@/api/user";
import Dialog from "./components/Dialog.vue";
import authImg from "@/components/authImg.vue";
import { downloadsignoffApi } from "@/api/common";
// 拖拽插件
import draggable from "vuedraggable";
// pdf插件
import { fabric } from "fabric";
import * as pdfjsLib from "pdfjs-dist/build/pdf";
import workerSrc from "pdfjs-dist/build/pdf.worker.entry";
import * as pdfjsViewer from "pdfjs-dist/web/pdf_viewer";
import { fa, it } from "element-plus/es/locales.mjs";
pdfjsLib.GlobalWorkerOptions.workerSrc = workerSrc;export default {components: { draggable, Dialog, authImg },data() {return {// pdf地址pdfUrl: "",// 左侧签章列表mainImagelist: [],// 右侧坐标数据coordinateList: [],// 总页数numPages: 1,defaultNumPages: 1,// 当前页pageNum: 1,// 缩放比例scale: 1,// pdf是否显示isFirst: true,isShowPdf: false,// pdf最外层的out-viewoutViewDom: null,// 各页pdf的canvas-layoutcanvasLayoutTopList: [],currentDragPosition: {x: "",y: ""},// 用来签章的canvas数组canvasEle: [],// 绘图区域的宽高whDatas: null,whlist: [],// pdf渲染的canvas数组canvas: [],// pdf渲染的canvas的ctx数组ctx: [],// pdf渲染的canvas的宽高pdfDoc: null,// 隐藏的input,用来提交数据shadowInputValue: "",points: [],activeEl: null,activeobjectData: {type: "textbox",text: "文字批注示例",fontSize: 20,left: 200,top: 400,width: 100,fill: "#000000",textBackgroundcolor: "rgba(0,0,0,0)",opacity: 1,stroke: "#ffffff",strokeWidth: 0,background: "#7ED321",scaleX: 1,scaleY: 1,fontFamily: 'Microsoft YaHei',underline: false,linethrough: false,overline: false,textAlign: "left",lineHeight: 1,charSpacing: 1,cornerColor: "#3B82F6",cornerStyle: "circle",borderScaleFactor: 2,transparentCorners: false,rotate: 0,selectable: true},isVisible: false,title: "",deleteIcon:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAYAAACtWK6eAAAAAXNSR0IArs4c6QAAD5hJREFUeF7tnXuMXFUdx79nduduW56Rd+ncLZQyd7eggKItlAgBgSiPFCUBFCKCAWwNGB4GjGlJlABWwCAPIyJKeCjyRsMzoBaKqCCPdmcppd07LRABI0jazp3d+zOzbZPy2J17z7lz58zZb5P+1fM7v9/5nt/nnN85d3qvAv9QASowpgKK2lABKjC2AgSE2UEFxlGAgDA9qAABYQ5QAT0FuIPo6UarCaIAAZkgE81h6ilAQPR0o9UEUYCATJCJ5jD1FCAgn6DbwPRJ0wsiX4klLiuoXRt/Y8huCtgVwNZ6Ultj9YEAbxWg3hTIW42/BVUYjJX6Y9/qDautidKSQAjIpokYmNZ9aKFQ+CKALwvweUvmJ9cwFPAcgD/FcfznvjXDT+Xq3FJnEx6QwV5vngguAHCQpXPUrrCeUQqLy0PRve0KwAa/ExaQyjTv0yiMgnGqDRNhcQy3IsbiYE30ksUxtiy0CQnIgO8tUsD5DpwnWpYYH+m4cW75aV8YLcrLoS1+JhwgAyXvHqUwz5YJ6KQ4RHBvXzU6oZNiNo11wgCyunfKbjUZ/qsAM0xFm8j2CljZo7oPmT607s2JoMOEAKTidx8MFJZMhAnNb4zx3CAcfjo/f+3x5DwgjWcaKo5XtUdet71KobCH689OnAZkcCp2lG5vJYBt3U7Vto3ufTUczSi/gXfaFkGLHTsLyDPTMPlTBa9xh39UizWc6N0/8p84mnfQGqx3UQhnAan43l0AvubipFk4pj8EYXSihXEZh+QkIAMl72SlcLuxOuwgsQIiOKWvGt2R2KBDGroKyLNK4QsdMgdOhCmCv/VVo9lODGaLQTgHSMXvOReQa1ybqM4YjzovCGs/64xYk0XpFCCvzdh65+F69CyAPZINP1GrpaLkWSVqaddI19KZa9evSWRlaaMVu0+eNtI1MkeUzFGiGiv+nAxDXdVd9GbvtfKDf2fYZ1u7cgqQwd6e+SLy8ywU3fjT78Il5XDDE1n0Z2sfjZ/5K6UWQqlDs4hRKbWgPFS7Lou+bOjDKUAqvnc/gOOMhRUsDqrRhcb9dFAHGZamDwRhdHwHDX3cUJ0BpDoNk9cVvHcFmGwyOUEYOaOJjg4V31sLYKqObcNGAeunxNEOJUeeiziTDAO93glKcLfuxI7aiTo+qNYeMOqjw42Xl7x9CgovmwxDFL7aNxTdY9KHLbbOAFLxe34JyJnawgpuDqrRGdr2DhlWfO9KAAYlpropCGvfdkEShwDx/g7gc3qTot5BF+YGq2qDevZuWa3cc5IfDY88p6B20RzZP4IwOlDT1iozlwBpXL/urqOuAm4ph9HpOrau2gz63q8F+Kbm+NYGYTRN09YqM5cAGQbQpaOuUuq75aFaJtfDOv5ttBns7VkgItdqxjYShFG3pq1VZk4AsmJX7DTiedoPp0YKsv+s1fV/WTUzbQ5m2fTifl2xekE3jK4o2nnmW3hb194WOycA2fSGkhc1RX03CKMdNW2dNqv4XuP/eeygNcgYn3HhTShOADL6NLhQeFJrIkWeCqr1w7RsHTeqlIpP6j5hlzg+zIWXzxEQAjIm5gRk9MFn5//hDtKaOSQgBATgDsIdZJz1hTsIASEgBGQcBQgIASEgBETnlMIzCM8gPIOMQw4BISAEhICMu7nykM4zCM8gPIPwDMIziI4CGZVYoz9sExygYvQL8Fm9UAytdF86IMJv8Y0nfYfpqoB/SgHLu4AXZg7VtX9suVkS4xLr1VLx9FipqwBsb5jiNKcCWSqwToCfmH4VywiQTZ8yW5jlqNgXFchYgUeCMDpat09tQIx+/6QbLe2ogIYCseCk/mr0Ow1TvR8rbnrzxSMmr4fRCZY2VEBTgfWIMVvn/6do7SCVUvFsKHWDZrA0owL5KyByTlCt35jWsS4g10Gp76R1xvZUoG0KiFwfVOvz0/rXBUT7f5qlDZDtqUAmCmg+ENYCZLBUXCxKnZ9J4OyECuSggBL5ablavyCtKy1AKr3Fb0HUr9I6Y3sq0DYFlJwRDNVvTutfC5BNr4RpvCSBDwfTKs727VAgipXM6R+qP5/WuRYgDSebnqCnJjJtgGxPBUwVUMDl5TC6WKcfbUAazvgkXUdy2uSswINBGGl/M8YIkFFINr6T6jY+NMx52umuqQICObsvrP+iacNxGhgD0uh79Mk6ZC6AfQH0awWk+6tRLWc0sl4B3V9Zq8JgDFQALOsPa4+ZjjMTQEyDaNhXfE+y6Id9uKGALV/6IiBu5JNzoyAgH5lS7iDO5bjRgAgIATFKINeNCQgBcT3HjcZHQAiIUQK5bkxACIjrOW40PgJCQIwSyHVjAkJAXM9xo/EREAJilECuGxMQAuJ6jhuNj4AQEKMEct2YgBAQ13PcaHwEhIAYJZDrxgSEgLie40bjIyAExCiBXDcmIATE9Rw3Gh8BISBGCeS6MQEhIK7nuNH4CAgBMUog140JCAFxPceNxkdACIhRArluTEAIiOs5bjQ+AkJAjBLIdWMCQkBcz3Gj8REQRwFRwDIBljf+KqC/8VeAWUbZMoZxnr5aEf94fRIQ9wAZEFHf66vWGh83/dCfgVLPUUrJ1QD6Mkq0PH1lFHK6bgiIW4A8UOyOTpvxOt4bLw0GfW+hAIvSpcqHWwtwaV8YjdvHyj2xXX3Y+y0A7beam8SYhS0BcQqQeG4QDj+dJDEqvd4gBHsnafuxNgqvBkNROYltxe8+GCgsSdLWxjYExB1ArgzC6PtJk+xVv+eYGPJg0vZbtitAHbt3WHsoqW3F964AcFHS9ja1IyCOABIL9u2vRq+kSa6K760BsHsaGwBrgzCalsZm9LMUCi+nsbGlLQFxA5CVQRjtlTapKqVi+s9oa37GuOJ7rwGYkTbGdrcnIG4AcmsQRqelTaaK33MDIGens1M3BmHtnHQ2o99daRzWT01r1+72BMQBQJLcKH1Soul82zFPX+2Go+GfgBCQhWkSkYCkUSu7tvzClIGWeSZtnr4MJMnMlDsIdxDuIOPgREAICAEhIMl33E78RmGeZU+evpLPWutacgfhDsIdhDtI8hWGO8j4WnEHSZ5LWbbkLZaBmnkmbZ6+DCTJzJQlFkssllgssZIvKCyxWGJtqQB3EO4g3EG4g3AH+SQF+Fus5nnBHYQ7CHcQ7iDNV4rNLXgG4RmEZ5BxcoCAEBACQkBGFeAZpHllwTMIzyA8g/AM0nyl4BkkmUZ8kp5Mp6xb8acmBormmbR5+jKQJDNTllgssVhiscRKvqDwFou3WLzF4i0Wb7ESrpkssVhiscRiiZVwucDoC84keWs7WuZ5cM7Tlw3qcgfhDsIdhDtI8rWIOwgP6Tyk85DOQ3rCNZMlFkssllgssRIuFzykNxWKh/SmErWkAX9qYiBrnkmbpy8DSTIzZYnFEoslFkus5AsKb7F4i8VbLN5i8RYr4ZrJEoslFkssllgJlwveYjUViof0phK1pAFvsQxkzTNp8/RlIElmpiyxWGKxxGKJlXxB4S0Wb7F4i8VbLN5iJVwzWWKxxGKJxRIr4XLBW6ymQvGQ3lSiljTgLZaBrHkmbZ6+DCTJzJQlFkssllgssZIvKLzF4i0Wb7F4i8VbrIRrJkssllgssVhiJVwueIvVVCge0ptK1JIGvMUykDXPpM3Tl4EkmZmyxGKJxRKLJVbyBYW3WLzF4i0Wb7F4i5VwzWSJxRKLJRZLrITLBW+xmgrFQ3pTiVrSgLdYBrLmmbR5+jKQJDNTllgssVhiscRKvqDwFou3WLzFcuwWS0EeK4f1I5MvAxtbDvR6dyjBSWnsROHOvqHo5DQ2jbaDfvFRgfpSWrt2t2eJ5UCJBWDdtsPRTlPfwLo0CTXge88rYP80NgK80BdGB6SxeWMqprzf7b0NYEoaOxvaEhA3AIESzCtXo/uSJlWld9IekPgVjaRdB1XYJxjasCqxr1LPcVByf9L2NrUjIK4AAiwrh9E+SZOrUvJug8IpSdt/qJ3g9qAafT2pbcXvWQLIwUnb29SOgDgCSGMYClhUDqNLmyXYCt/rHwGWNWs33r93AbNmhtHyZn1UfO8iAFc0a2frvxMQhwAZHYrIOUG1fuNYCbfMn3R4F+LHs0jIERSOmBVueGKsvip+z7mAXJOFr3b1QUBcA2TjVvIwYvl9V73+0My38HZlR2yDKd7RgDoSkDOzTTZ1EyCPYl30cPAO/rfZlwDHKOC0bH3l3xsBcRGQLcck8hSUOjSX1MrTVy4DAgiI64DklEiuuiEgBMTV3M5kXASEgGSSSK52QkAIiKu5ncm4CAgBySSRXO2EgBAQV3M7k3EREAKSSSK52gkBISCu5nYm4yIgBCSTRHK1EwLycUDeB7CNqxPOcaVSoBaE0aRUFi1qbNNLGwYABC0aJ7vtKAXUO0FY28mGkC0CpPg4oA63QRTG0F4FFPB6OYxmtDeKjd6tAWTA937jwq9QbZjUjo9B8GJQjfazYRzWADLoe5cJcLENojCGdiuglgRh7ZB2R2HVDrLcn3REAfFjNojCGNqsgOBHQTX6YZujsKvEEqBr0Pf+C2BrG4RhDO1UIJ4bhMNPtzOCzb6tKbEaARm90MAGNRlDFgqsCcKolEVHWfRhFSADfvEsBTXm/+vOYsDsw24FBLilL4xOtyVKqwB5berk0nD3yEsAtrdFIMaRrwIFqGP3DmsP5et1bG9WAdIIc7BUvFaUWmCLQIwjVwUeDMLouFw9NnFmHSDLpxdnF2K11CaRGEs+Cti2ezRGbR0gG3cR725ROCGfaaEXSxSwbvewFpCB3u45SgoPA9jWksljGK1VYDiW+PD+6vBfWusmfe9W7iCNYVR87xsAbk0/JFp0mgIKuLgcRpfbGLe1gGyEpOc8QK62UTjGlI0CIri9L8ULubPxmrwXqwFpDGOg5F2iFH6cfEhs2TEKKNwXDEXzbI7XekAa4r3qF8+K+QDR5jxKHZsoXNY3FP0gtWHOBh0ByGi51dtzIUSuzFkfumuBArpf7G1BKE277BhARiEpFc+GUvMBJP5gTVMF2CBPBVYqkavK1fr1eTo18dVRgDQGOvrdvWLPfIllgVLwTQZP29wUqAlwdXcUXdX4LERuXjNw1HGAbB7z63tstUsUD58GkRMBHJiBFuwiewVWA7grVnJn/1D9+ey7b32PHQvIltKsmF6cHcc4JRZ1oFLYDsB2AmyngK1aLyE9KGADIO+JUu8pwXsCrMLG7yl25AdEt5xRJwBhilKBVilAQFqlLPt1QgEC4sQ0chCtUoCAtEpZ9uuEAgTEiWnkIFqlAAFplbLs1wkFCIgT08hBtEoBAtIqZdmvEwoQECemkYNolQL/B17ME0Gk2Cy/AAAAAElFTkSuQmCC",samllIcon:"data:image/png;base64,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",largeIcon:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAYAAACtWK6eAAAAAXNSR0IArs4c6QAAIABJREFUeF7tfQuYXFWV7lqnqpsE0bSjqFdlZnBUIm+uDxRIEEHBOyqCJr6uEnWMEFJ19u4OiTCMtg8gId2996nuROxxMJHRQaMTwRczg4IDKIgPAiogPrjqBa6C05kxIY+qs+634PTQIf2ovc+7au/vq6/zfVlr7bX/tf86dfZjLQTXHAIOgRkRQIeNQ8AhMDMCjiBudjgEZkHAEcRND4eAI4ibAw4BOwTcE8QON6fVJQg4gnRJoN0w7RBwBLHDzWl1CQKOIF0SaDdMOwQcQexwc1pdgoAjSJcE2g3TDgFHEDvcnFaXIOAIklKg16xZs6DZbPY1m80FANDned4CIupDxAVhGPLfXQAwQUTbPc/bzv8Ow/Cxv9u3b9++adMm/n/XckbAESRGAIQQfa1Wa2GlUllIRIchIv9dyH9jmJ1UfRgA7ok+9xLRPZ7n3aOU+kUCtp2JNhFwBGkTqOXLl/fMmzfveM/zTgGA10REeE6b6kmKNZk0iHg7Ed3QbDZvHhsb+3WSHThbTyDgCDLDbFixYsVBTAgiOj4Mw0WI+BoA6C3o5LkXAL6NiLft3bv3trGxMX7yuJYAAo4gU0CUUv4ZEZ2NiGcR0UkA8LQEMM7DxE8R8Toi2qq1viUPBzqlT0cQAJBSvo6JAQBnAcCzOiW40Ti+g4hbW63W1kaj8ZsOG1vqw+laggghXhI9KZgUL0sd6fw72MFPFM/z/lkptTV/d8rhQdcRREp5GhG9BwDeXY4QpeLlNiK6avfu3VddccUVv0+lhw4x2jUE8X3/nUwKRDyjQ2KXxDAeRMTPAsBVSqmfJmGw02x0NEH4pZtJET0x/menBS/B8TT5icKfRqNxQ4J2S2+qYwni+/4AIvoAcEjpo5TtAK5BxEuVUt/Pttti9tZxBKnX66d4nncRAJxWTMhL4dVuIrr06U9/+qWDg4O8Mdm1rWMIUqvVnlatVi8iojVdG83kB/49ALhEa/315E2Xw2JHEERKuZSI+KlxTM6w/xwAtvGHiLYh4iMA8CgRPfbp6enZiYiPHnTQQY8+8sgjPZVKZT4AzEfEx/5O+byYiHgsx0ZjynXDEhHH+ImitX4wZ3wz777UBFm9evVzd+/e/QlEfG/myAHcCQDfR8QfRmS4Qyn1aBp+rFq16tBmszlJmFcAwPEAwAsQWbZfENHHgyDgVa+uaaUliJTyTCK6DABekna0iOgBPhwIALdHf3+glPpj2v3OZr9erx8JAC+vVCovI6KX878z8me0Wq1eODQ0tCOj/nLtppQEkVJ+nIguzgC5LUS0JQiCLRn0FauLgYGBw1ut1hIA4M8RsYzNoUxEtyLihVrrG9Pspwi2S0WQWq12XKVS4afG6SmCxxtmWyqVypbh4eGfpdhPaqZ931+CiJNkSasfXt1ikgyl1UER7JaGIL7vfwARmRzPSAm4rxDR58vwtGh3/FOeKssA4C/b1TOU+yI/TZRSvzLUK4V4KQgihFgPAKtSQDQkos2e521WSn0nBfuFMMnXf3ft2nUOIp4DAGmcKOAbj8uDIPj3Qgw4QScKTxAhBK+aJHqwkIge8TxvEwAwMe5KEM/CmxJC8Hm0c4jo1ISd3U5EH+ikJzDjU2iCCCH+BQBel2QgEXF9s9lsjI6O/i5Ju2WzFa0C8qbqqxL2/b1aa/7y6YhWWIIIIXjD7eikUCaibwDAuk78GWCL0eDgoDcxMcEk4Q9nX0mkEVEtCIKxRIzlbKSQBBFCPAQAz04Im/sRcZ1S6oqE7HWcGSnlUdERnXclNTgi+lAQBOuSspeXncIRRAhBSYFBRGNhGK7r9p9T7eLp+/7bEJGP7CTy5O4EkhSKIEKI+wDghe0GdBa5hxFxlVJqcwK2usrEBRdc8Jw9e/YMIyJfMIvdiOitQRB8ObahnAwUhiBCiOsS2gC8MQzDVY1G44c5YdoR3QohLgSAS5MYjOd5p42MjHwrCVtZ2ygEQYQQ/EJ3ftzB86nTZrO5anR0dHdcW07/sWwvbySiYQB4UVw8OLdYo9G4Oa6drPVzJ0hSm4BEdG4QBJ/KGsBO72/lypWHVioV/snF2V/itD2IeKJS6gdxjGStmytBkjp0SERLO22DKuuJMFd/QogvRgch5xKd8f8Rke/LvLVMm7O5ESQ6WzVujXak6MgRF8H29ZMgCQDc3Gq1TivLz+BcCBKdyv23BA4enqC15muhrmWEQBIkQUSllOrPyOVY3eRCkCRWrFqt1iFufyNW7K2Vfd/fgIgrrA3wGSfEdymlPh/HRha6mRMkifcOrXXmfmcRjDL1IYQYBICPxPD5N2EYntZoNHjvq7At04kWHZD7Shw09u7d+8wNGzZwMgTXckYgLkkQ8ctKqbfmPIxZu8+MIJxgYc+ePdfHuUMehuGLi/6NU+Rgp+FbAiThy1Zr0/AtCZuZEcT3/SvjZB9ptVqvGh0dvTWJQTsbySIghNAAwFksrRoiLlZK3WSlnLJSJgSJ8lZ9wXYsnuf99cjICB9Xd62gCMS52EZE1wVB8PoiDi11gnDGw0qlwlcxrZK6uR3yIk6b/X1atWrVU5rNJl9wO9HGY0QUSqnARjdNndQJIqVca5sOlM9WKaVqaQLgbCeHgO/7xyMik8T48hUi/n7v3r0nF62+YqoEiRJJf9syBDe2Wq0zyrLjajnGjlPzff/9iPhpy4FdpbXm4kaFaakSRAjBu+U2WdYfDsPwDHdkvTDzxMgRIYQCAGGkFAkT0buCICjMBmJqBInqc1glFUPEZe6yk830KoZOdNedf2rZfDlyhV5e1co1teskkqkQJCqnfIdN8Rq+JhsEgXvvKMZct/YiOm/HS7dPMTWCiH+rlErkspZp30+WT4sgPhHx2rhpu7/Vai3q9DNWUkquw86reidznlvP8zg313VF+dY0DdpM8kKIvwOAj5naQ8R7ms3msUV4/0yFIEIIvu5qnMEPEc/r5OwjUsoXEtHVAPDSaSbNAwCwWmv9OdMJVVT5wcHB3omJCb5FaJx5PsrU+Pd5jy1xgnA1WUQ0DjLnrQqC4K/zBiSt/mu12vMrlcpv57KPiK9XSvH9/I5ovu+/BRG/ZDGYm7TWiy30ElVJgyDftCm1TEQnd2pStyVLlvQ+73nP+xoAvLbN6L1Ua/2jNmULLyaE+AwAcAJto4aIb1ZKXWOklLBwogSRUp5GRLy0a9Q4HahSarWRUomEhRA8OXiStNtGtNYD7QoXXW5gYGBhq9Xin1pGmfmLcNo3UYLYnMfhRNJhGPILWcfmypVSBkRUN5jIX9Bav91AvvCiQohPAMDfWjia663RxAgihOBSaMYFZxBxWCmVRmkDi1ikoyKEuAEAXm1g/Uat9SkG8oUXrdfrL/I878cWy74btNYr8xpgYgSRUnIJ5ksMBxIi4rFlynJhOL7HxB1BHkfN8qruAzt37jx8fHx8uw32cXUSI4gQgotcvszEISL6TBAE7zPRKaOsI8jjUZNSvoKIbrOIYW4lFRIhiJTydUTERwuMGiK+upMrO02C4QjyxLQQQvA5q3cYTRSAa7XWZxrqJCKeCEGEEFxa4IOGHn1Fax03W59hl/mIO4I8gbvv+6cjovE+TxiGRzUajZ9kHcHYBInOXd0NAM8ycb6bEr45guw7MyzwACL6SBAExsdWTObkdLKxCSKE+BsAMD0S8FOt9ZFxnS+LvsWE6LhVrKmxklKuIKINhvG7Q2t9nKFObPHYBJFSfp2I/pehJx/VWnNepa5ojiD7hrlWqx1cqVS4Hv3BJhOAiM4IgsD4XdekjyfLxiJIdN+cN/ieauJEpVI5Ynh42HjPxKSPIsk6guwfDZslX0T8mFIqTrI642kRiyBCCD5cyGeMTNoWrfVSE4WyyzqC7B9BIQRvnPIGatuNiL4VBIHNJay2+0j0CeL7/iVRTbu2Heiml/NJUBxBpp8eFrg8unPnzmeOj4/vbHvCxRSM+wQxPULxoNb6uTF9Lp26xUTo6Jf0yQBKKQeIyOhadtbvIdYEiS7D/AkAetqdsZ1+52MmHBxBZnyCvAYAjGoXIuKlSimbQ4/tTtN95KwJYnm0/RKt9cVWnpZYyRFk+uAtX778wPnz5z+CiPMMwnuz1nqRgXws0TgE+SgRfdikdyI6MwiCa010OkHWEWTmKEopbyGiEwziHPb19fUMDg6GBjrWotYEsQg6VCqVg4eHhx+29rakihZYdcU7CIdTSjlKRKbH2U/RWt+YxXSwJojv+w8i4nMMnLxba324gXzHiDqCzPoEeTsR/ZNJsMMwPK/RaPD5v9SbFUGEEH0A8B+G3l2ptX6/oU5HiDuCzBzG/v7+Q8Iw/I1hoLXWWhrqWIlbEaRWq72yUqkYFc/s9JQ+s6HvCDL73JRSPkhEbf8aybJcghVBLJIQMEK53i22+vpISMkRZHYgLYq63q+1PjSh8Mxqxoogvu9fhogfMnEQEQ9USj1qotMpso4gcxJkHSfNM4n3xMTE/E2bNu0y0bGRtSKIEGIrALzZoMOfa60PM5DvKFFHkNnDaZls8DitNed/TrVZEcT3/bsRcaGBZ113QHEqNo4gs8+UgYGBw1utFh9/b7uFYfiORqPBaVxTbVYEEUKQoVcXa61NM54YdlFccUeQuWMjhNhjcmwJADK5U2RMkDVr1izYvXv3xNxDfkKCiN4YBIHpsXiTLgot6wgyd3gssuJkstRrTJCBgYG/aLVa98895H0kunYFi1FwBJl7tpiuZCHiZ5RSqaeMMiaI7/tHI+K2uYe8j0QmL1SGPmUm7ggyN9SmCz9EtDUIgrPnthxPwoYgixHxOybdEtHCIAjuNdFJQ7Zer5+KiGcjIl/+f1UafZTEJl+T5o3enxUlN4BFvqxva61PTRtvY4JIKd9IREYncsMw/ItGo2F6nCDRsff399fDMCxcHe5EB2ln7Dat9SvtVJPTEkL8AwCY/GT6kdZ6ukJEyTkFAMYEEUK8GwA+a+JF3qd46/X6CZ7n3WLic5fJ5p6GSQgxBgDnG+D+S631Cw3krUSNCSKlXElEoya9VavVg4aGhnaY6CQlu2LFioN6e3tvBYAjkrLZiXbyPisnhFgPACZZ/h/RWj8z7VgYE8T3/YsR8eMmjvX19VWyuuDyZL+klO8nItvC9ibDLLtsrk8RIQRnTeSin+22pta67eve7Rp9spwxQYQQzHJme9ut1WrNy6tiqRDiUwCwvG1nu1vwuVrrB/OAQAjBZZ8vNOj7T1pro3xsBrb/W9SGIDapRp+utTbaXLQZzHQ6QgiucfempOx1uJ3caiMKIRRvGRng+zut9SEG8laixgTxfX8JIn7RsLfcvpksC/sYDq8jxHfv3LnzqePj43vzGI3Fk/4nWuuj0vbVmCA2tUAQ8a+UUr9KezAzPEH41DGfPnZtFgSI6LtBEJyYF0gW9S0zyW5iQxDjKkGIeKRSyui0ZpKBEkL8AABSXzNP0uccbH1Qaz2eQ7+PdSmE2AIAb223f0T8mlLqje3K28oZE8T3/cMQ8R6TDhHx5UopnqS5NCnlC4novlw6L0en39Ram2boT3RkQgg+zMq5nttqiPiPSinek0u12RDk2Yj4kIlXiLhYKXWTiU7Ssrwf0tPTsx4Rz03adont/RIRL1BK5f4TVAjBGRY502JbDRHHlFK1toRjCBkTZNmyZfP6+vqMrs4i4t8opfgoQe6tVqv9VbVaPZqIjsnYGZO0/Zzzyei8m8lY+Auu1Wrd2Ww279y4cSOnj829CSF+CwDPb9cRRPyEUspk36Rd0/vIGROEtYUQfwAAk13MUa113crDDlASQnCxICOCdFqd9NnCeP755z+jp6fHKKFgVrmxbAnCP5dOMpi7N2mtFxvId5SoI8js4RRCGCexDsPw9Y1Gw7gYqOnEsiUI1yTkDcN2239qrRe0K9xpco4gs0dUSimJaMQk7s1m8yVjY2NGi0Um9idlbQlifNykWq2+YGho6Nc2TpZdxxFkzifIZgB4j0mcC532x/f9NyDiV00GBABnaa2/YqjTEeKOIHMShNP3tL1oQkQPBUHwP7KYHFZPEMt9hUyyUGQBmmkfjiBzEsQoSw4R3RoEQSY3Qq0IEq1k8ZmdqsFkyX0zysDXREUdQWaG0/f94xGR7+u03Yjo6iAI3tG2QgzBOAS5CwCONOj7j1rrZxjId4yoI8jMoRRC8GZfwyTYRLQ2CAKTo/Em5veRtSaIlPJKInqvSc9hGB7VaDR+YqLTCbKOILMS5B8B4F2GcX6D1vrrhjpW4tYEsbmbHobh+xqNxmesPC2xkiPIrAThM3JGd8uzvMJtTZCVK1ceWq1WjY6wI+JGpZTJxfwS0+IJ1x1Bpg8jH/upVCq/MAxyJul+Jn2yJggbEELwRo1J1vbbtdavMASk9OKOINOHUEppXH4NES9USq3NalLEJchGADjPxNlKpXLE8PDwz0x0yi7rCDJ9BH3f34CIK0ziS0QnB0Hw7yY6cWRjEURKeQ4RbTJ0YFBr/VFDnVKLO4LsH75arXZwpVLhS3QHGwT39319fc8bHBxsGujEEo1FkJUrVy6sVqt3G3qQa3oZQ18TEXcE2R9GKeUKItpgCPCXtNZLDHViicciSPQewsu2RknZiGhpEAR8xbIrmiPI/mG2SOjNRupaa6OkhXEnWGyCSCmHiGjA0JGuqjjlCLLv7BBCvBoAbjCcM01EPCzr5B+xCSKE4EwYNxsOFrrpZd0RZN/ZYfNyDgDXaK1N6mKaTslp5WMTJPqZxVdETzb0qGte1h1BnpgZK1aseE5vb++dhi/nQETLgyDge0iZtkQIIqX0iUgben7/AQcccOy6deu2G+qVTtwR5ImQ+b6/BhFN9zF29Pb2vvjyyy9/IOvgJ0KQer3+557n8d7GU0wGQER+EARGB9VM7BdF1hHk8UgMDg72TkxM8N2PlxjG5ota67cZ6iQinghB2BPf969CxP9t6FUmRVAMfUpc3BHkcUiFEJxEnJOJGzUiOicIAqOaNEYdzCKcGEGklGcR0T9bOPYerfVVFnqlUXEEeTxUvu/fgognmASObw82m80jN2zY8IiJXlKyiREk+oYwujrJOoj4LaXUaUkNqIh2LJISXKu1PrOIY7H1SQjBaUWN976yvPsx3dgSJYjv+wOIOGQKIiK+WSnFZQo6sgkh+Pbb5w0G90mttdEZJQPbuYhKKb9KRG8w7PxRIjouzwKwiRLk3HPPfda8efP4KWJ6of57fX19J+VVhcowaMbiXF3X87zrDRQ76v6+EMKmpgzDlfsXRaIE4RFJKdcS0RqDyTApepHW+jILvVKoCCGuAIAPtuFsIarOtuFnWyK+73MuZ95INroUFf38Pl4p9f22OkpJKA2CHEFE/BQxSejAw9uOiIuUUnzXvSObEIILD8122K7jDnJKKUeISFoE9HNaa9NVUYtuZldJnCDcne/7VyKi0X31yM1CgJI4ylMMRgWIOFfv1LQ1d0W3Lfkp0zHNJqXo5OAR8bVKKZOfpanglgpB6vX6KZ7nfdvGYyJ6exAEX7DRLZPO6tWrn7pnz56Xzps3b9vatWv/o0y+t+urEOJfAOB17cpPkcvl3NV0fqZCEO5ICMFZFG2WKu/s6ek5ff369UY1SCyC4FRSRMD3/QsQ8XKLLpqe5y0aGRkxypVl0U9bKqkRRErJpdr4auQBbXkyRYiIPh8EgWkqGNNunHxKCAghXg8A37Axn1Xdj3Z9S40g0bvIhxHR9nptR69qtRugssn19/cfQkTXEdHhpr4j4o8rlcqioaGhHaa6acmnSpDBwcHqxMQEP0Ws8qgi4puUUqZJstPCytltAwEhBL8/Lm1DdD8RRHybUsq0xLhNV23rpEqQ6F2ECzNygUabdl+z2Tx9bGysK8sm2ACWp44QgkuifczSh89qrc+x1E1NLXWCsOdSylEiWmkzCiLaGgTB2Ta6Tic7BHzfX4KItt/+fBBxkdbaNAFI6gPMhCBCCD56wj+1jHdTIwS66g576lFPuIOY5ODbgrUgCMYSdisRc5kQhD31ff89iMiVhGybI4ktcinq+b5/OiJa1wrMqt65LQSZESR6H+Hbg3FqWzuS2EY6Bb2BgYHDW60WJ3+zbfd5nnfqyMgIl4AuZMuUIKtWrXrK3r17r0fEV9qiQUQbgyDougTYtnilpVer1Z5WqVTi5hNYorX+Ulo+JmE3U4JETxHOifRvFocZp463o46DJxHILG1IKecT0c6YfQ5prS+IaSN19cwJEpHEuEruNEg4kqQ+PfbvoFarPb9SqcT9SXRTX1/faYODg3tyGIJRl7kQJCKJ9YbSlBE6khiFO56w7/tHI+K2eFbgd3xGT2v9o5h2MlHPjSBSyhcQEZ/XMakvMh0ogdZaZIJWF3fS399/ShiGVie0J2FDRH5inKmUsl71yjoEuRGEB+r7/mJEvBYAFsQc+FXVavW8Ip3hiTmeQqn39/e/JQzD2C/TiLhMKRVnqT9zXHIlSESSODuwUwG7hZNoB0FwW+YodnCHMY+PTEVmjdba5vh7rujmTpDofWQZACRR3HN7RJJ/yBXVDuicT+WGYcgZaqwOHj4JghGttWkFgEKgWAiCRE+SlYiYVO0H3dfXN9CpWVLSnjl8n4PTN9kcWZ/Gt1Jfoy4MQSKS2CQ2nmm+XN9qtVaPjo7+OO0J1Un2Y9wEnBYGIiJEXFr0DcGZYlgogqRAEr54s66vr29dGdbc8yRalGCB0zXZ3CGfy/UW/1TTWtukpp3Ldqr/XziCRCR5CyLGXjWZgtztRLQuCIIvp4pmCY1z3irP89ZYpuYxGXEzDMOljUZjq4lS3rKFJAiD0t/ff2oYhkmnfdlUqVTWDQ8Pc333rm9RxkN+atheQzDFcA//3CpTmtnCEoSRr9frJ3me9y0A6DWNxCzyfDnnijAMNzcajfsStFsaU5xIGhG5hLdprtwkxrg7KuLK+1+Fb4UmCKMnpXwZAHyOiF6cMJo7iGiz53mb805vmfC4pjUXFa9ZxrU2TEsQpOAfJ6XmSse2V7FTcGl6k4UnSESSo/iYOwCclBIy/8RkCYKAE511VOOagD09PUwKvu9tWtkpNSz4NHC0uvX11DpJwHApCMLjrNVqB1Sr1ctSfpm8ERG3NJvNLaOjo39IAN/cTHCpZSLiUwqcC/jg3ByZveM/Ratb3yyof1AagkwCKKV8JxFxFvg/TxHUPxDRFiaL1por+Jai1Wq1g6vV6hImBgDwvZvUGhGNIeKzEthp/8/o51Yhn96lIwhHvF6vv6hSqfDT5C2pzYAnDPNT5WtE9OOdO3feOj4+HveiUKIu+75/mOd5Lw3D8MSMnhb/BwA+obX+NA9ECHE1AMQtsMmZ/Xl1618TBScBY6UkyJSnyYeip0kCUMxtgoh2eZ7H9xj4w/X2bsn6PrUQ4kQA4M/xiHgcER06t+eJSXzV87y/GxkZ2edOiBCCq2dxFS3rhoicwJtJkvTSvrVPrFhqgvAApJSLwjC8CBHPiIWEpTIiPkREPGG28d9qtXrH8PAwl8SO1c4///xn9PT0HIOIxxDRsQBwTPSJZTeG8sVa60tm0vd9/3OI+M4Y9ln1j9FmIi/tF6KVniBTniY+AFxERPy7OO+2lwkDALzn8ugMHy4wNB8ADoz+8r8nP7yk/fy8B/HYNyjiVj64ODIy8t25/JFSXkVEcYvePOx53tKRkZEb5uovi//vGIIwWCtXrlxYrVYvAoB3ZwFeh/dxFxENmdYnF0JwPfO4+PMKIp/dyn2BpKMIMjlhfd9/JyIyUY7o8EmcxvD4iTfU29u7/vLLL/8vmw6EEJsAIG6e3f8XrW5xRs7cWkcShNGUUv4ZAJzL32ZEtDA3hMvT8X9x5ktEvHJkZCT2FQEhBF+A44tw1o2IHuJl6+HhYS4CmkvrWIJMoskbjJ7ncdpTfuwvygXlYnf6GwDY3Gq1No+Ojv4ySVeFEHyz830xbT4Q/dy6JaYdK/WOJ8hUVKSUXBKOnyhZ7J9YBSRDJa5EvHnevHmb06yR6Pv+pxHx/THH9X+5OrDW+nsx7RirdxVBJtERQnBBHy7xdhYAPNcYtXIrXIOIVyuleIMvkyalHCeiD8Ts7LetVmvp6OhoprULu5Igk4Favnz5ggMPPJBJwp83xQxgYdWJ6AEmBRFdHQTB7Xk4KoT4FAAsj9M3IvIuPm8mfj+OHRPdribIVKDq9fqRiHg2IjJZeGOuE9o3iehrnufxE+OPeQ/I9/1PIiIvnMRp9/ORGqXUD+IYaVfXEWQapLjmhed5J0Tnm06INvDaxTRvOSbFNyqVyjVZH4NpZ+BCCL62cF47srPI/Cracf9hTDtzqjuCzAHR8uXLD5w/f/4iz/MWE9FiAGDCeHMim40A71ncwUdcPM/7zo4dO64t2mHK6WAQQnA1qbglLH7ped6SJJakZwuVI4jhRB4cHPQmJiYWh2G40PO8w3iPBRF5n+UvDU2Ziv8KEX/GZOBPpVLZNjIy8nNTI0WRF0LELabEQ+Er07zjzityqTRHkIRgXbZs2by+vr6FEXGYMAsQcUEYhn38FwD6ohzEk385Fc5M57TuB4B7Pc+7p9ls3rtr1657x8fH+XxXRzUpZUBE9ZiD+jnffwmC4M6YdqZVdwRJA1Vns20EhBCKr5W0rTCNIBHdwwcclVJ3xbEzna4jSNKIOnvGCPi+P4KI0lhxX4W7o9WtODUT93PBESRmVJx6MghIKTkXcKwE14j43d27d5++ceNGvuueSHMESQRGZyQJBIQQ6wGAy/NZNyK6IgiCuMvI/92/I4h1KJxiGggIIdYBwOo4tvfu3fvMDRs28GW12M0RJDaEzkDSCEgp1xIRp0S1aoh4dFIv7I4gViFwSmkjIIS4FAAutOznhKRO/jqCWEbAqaWPgO/7l0Q3Q406q1arLxgaGvq1kdIMwo4gSaDobKSGgJTy40R0sUkH1Wr1oKQKujqCmCDvZHNBQEq/Amv4AAAA0ElEQVT5USL6cJudX6a15nwEiTRHkERgdEbSRkAIMQgAH5mjn+u11q9N0hdHkCTRdLZSRUAIwXkFeK/k2dN0tEJr/cmkHXAESRpRZy9VBHzfP9rzvFOJ6OXRtYNtiPjDtPL6OoKkGk5nvOwIOIKUPYLO/1QRcARJFV5nvOwIOIKUPYLO/1QRcARJFV5nvOwIOIKUPYLO/1QRcARJFV5nvOwIOIKUPYLO/1QRcARJFV5nvOwIOIKUPYLO/1QRcARJFV5nvOwIOIKUPYLO/1QR+P+7ykh9jnEpnAAAAABJRU5ErkJggg==",fileList: [],vissign: false,signlist: [],id: "",sts: [],baseIp: "",userInfo: {}};},created() {this.getsignimg();this.setPdfArea();},watch: {coordinateList: {handler(val) {},deep: true},vissign: {handler(val) {},deep: true}},mounted() {},methods: {getsignimg() {//初始化判断是否个人信息有绑定自己的签名章,userApi.getInfo().then(res => {this.userInfo = res.data.data;//如果没有则上传后获取图片地址,如果有就直接获取if (this.userInfo.signture != null) {this.vissign = true;const hostname = window.location.hostname;this.baseIp ="https://xxx.com/mis/api-docs/chairmansoffice/download";// /2025/2/F1339892_212757768635974367.pngif (hostname != "localhost") {var origin = window.location.origin;this.baseIp = origin + "/mis/api-docs/chairmansoffice/download";}this.mainImagelist = [{ name: "印章", img: this.baseIp + this.userInfo.signture }];} else {this.vissign = false;}});},/*** pdf相关部分*/// 设置PDF地址setPdfArea() {// 1. 获取地址栏//z这里的地址根据用户需求自己更改,不是真实可用地址// 获取pdf地址,这里区分是否开发环境和测试环境var fileurl = this.$route.query.fileurl;const hostname = window.location.hostname;var baseIp ="https://test-xxxx.com/mis/download";if (hostname != "localhost") {var origin = window.location.origin;baseIp = origin + "/mis/download";}this.baseIp = baseIp;this.pdfUrl = baseIp + fileurl;// this.pdfUrl ="https://test-xxxx.com/mis/download/2025/2/430956676170020900__2674826190452557870.pdf"//可以先定一个固定pdf地址进行测试this.showpdf(this.pdfUrl); // 接口返回的应该还有签名信息,不只是pdf},showsts() {if (this.$route.query.sts?.length > 0) {this.sts = this.$route.query.sts;this.sts.forEach(element => {const targetCanvas = this.canvasEle[element.pageNo - 1];if (!targetCanvas) return;// 创建文本的逻辑封装const createAndAddText = () => {if (!element.opinion) return;const scale = element.opscale || 1;const fontSize = element.fontsize || 14;const textConfig = {left: element.yopinion,top: element.xopinion,width: element.wopinion / scale,fontSize: fontSize,fill: "#000000",scaleX: scale,scaleY: scale,selectable: false,hasControls: false,splitByGrapheme: true,textBackgroundColor: "rgba(0,0,0,0)",fontWeight: (element.fontweight || "normal").toLowerCase()};const text = new fabric.Textbox(element.opinion, textConfig);targetCanvas.add(text);targetCanvas.renderAll();console.log("Created text:", text, "Config:", textConfig);};// 创建图片的公共逻辑const createAndAddImage = () => {fabric.Image.fromURL(this.baseIp + element.fn, img => {img.set({left: element.ysign,top: element.xsign,scaleX: element.scale || 1,scaleY: element.scale || 1,selectable: false});targetCanvas.add(img);});};// 执行创建操作createAndAddText(); // 有opinion才创建createAndAddImage(); // 无论有无opinion都创建});}},showpdf(pdfUrl) {this.canvas = document.querySelectorAll(".the-canvas");let currentPage = 1;// console.log(getToken(), "-------------");pdfjsLib.getDocument({url: pdfUrl,httpHeaders: { Token: getToken() },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"));});// 循环渲染pdffor (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宽高,缩放比例,渲染pdfrenderPage(num) {return this.pdfDoc.getPage(num).then(pageNo => {const viewport = pageNo.getViewport({ scale: this.scale }); // 设置视口大小this.canvas[num - 1].height = viewport.height;this.canvas[num - 1].width = viewport.width;// Render PDF pageNo into canvas contextconst renderContext = {canvasContext: this.ctx[num - 1],viewport: viewport};pageNo.render(renderContext);});},// 设置绘图区域宽高renderPdf(data) {this.whlist.push(data);this.whDatas = data;// document.querySelector(".elesign").style.width = data.width + "px";},// 生成绘图区域renderFabric() {// 1. 拿到全部的canvas-layoutconst canvasLayoutDom = document.querySelectorAll(".canvas-layout");// 2. 循环遍历canvasLayoutDom.forEach((item, index) => {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.width = this.whlist[index].width + "px";item.style.height = this.whlist[index].height + "px";item.style.margin = "0 auto 18px";item.style.boxShadow = "4px 4px 4px #e9e9e9";// 4. 拿到签名canvasconst canvasEle = item.querySelector(".ele-canvas");// 5. 拿到pdf的canvasconst pCenter = item.querySelector(".the-canvas");// 6. 设置签名canvas的宽高canvasEle.width = pCenter.clientWidth;canvasEle.height = this.whlist[index].height;// 7. 创建fabric对象并存储const canvas = new fabric.Canvas(canvasEle);// canvas.on('mouse:up', (e) => {// // 画布鼠标按下事件// this.getSignatureJson();// })s// .on('object:scaling', (e) => {// // 图形缩放时触发;// console.log(e.transform);// })this.canvasEle.push(canvas);// 按下鼠标const containers = item.querySelectorAll(".canvas-container");// 8. 设置签名和text文本输入的canvas的样式// containers.forEach(div => {// div.style.position = "absolute";// div.style.left = "50%";// div.style.transform = "translateX(-50%)";// div.style.top = "0px";// // console.dir(div);// 找到和class相关的属性// })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();this.showsts();},// 开启监听窗口滚动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滑块,重新渲染整个pdfsliderChange() {this.pageNum = 1;this.numPages = 0;this.canvasLayoutTopList = [];this.canvasEle = [];this.ctx = [];this.canvas = [];this.isShowPdf = false;// this.outViewScrollClose();this.whDatas = null;this.coordinateList = [];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"));});// 循环渲染pdffor (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();this.showsts();}, 1000);});}, 1000);},/*** 签章相关部分*/// 签章拖拽边界处理,不能将图片拖拽到绘图区域外// canvasEvents() {// this.canvasEle.forEach(item => {// item.on("object:moving", e => {// const obj = e.target;// if(e.target!=null){// const top = obj.top;// const left = obj.left;// // 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// );// }// var findIndex= 0// this.coordinateList.forEach((element,index) => {// if(element.cacheKey == e.target.cacheKey){// findIndex = index// }// });// console.log( this.coordinateList[findIndex ],'------------------ this.coordinateList[findIndex + 1]')// // 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 ][item] = Math.ceil(// obj[item] * obj["scaleX"]// );// });// if(this.coordinateList[findIndex + 1].name == '批注'){// this.coordinateList[findIndex + 1].xopinion= JSON.parse(JSON.stringify(top))// this.coordinateList[findIndex + 1].opinion= JSON.parse(JSON.stringify(obj.text))// this.coordinateList[findIndex + 1].fontsize= JSON.parse(JSON.stringify(obj.fontsize))// this.coordinateList[findIndex + 1].yopinion= JSON.parse(JSON.stringify(left))// this.coordinateList[findIndex + 1].wopinion= JSON.parse(JSON.stringify(obj.width))// this.coordinateList[findIndex + 1].hopinion= JSON.parse(JSON.stringify(obj.height))// this.coordinateList[findIndex + 1].opscale= JSON.parse(JSON.stringify(obj.scaleX))// }else{// this.coordinateList[findIndex + 1].name = '印章'// this.coordinateList[findIndex + 1].xsign= JSON.parse(JSON.stringify(top))// this.coordinateList[findIndex + 1].ysign= JSON.parse(JSON.stringify(left))// this.coordinateList[findIndex + 1].wsign= JSON.parse(JSON.stringify(obj.width))// this.coordinateList[findIndex + 1].hsign= JSON.parse(JSON.stringify(obj.height))// this.coordinateList[findIndex + 1].scale= JSON.parse(JSON.stringify(obj.scaleX))// }// this.coordinateList[findIndex + 1].scaleX= JSON.parse(JSON.stringify(obj.scaleX))// this.coordinateList[findIndex + 1].scaleY= JSON.parse(JSON.stringify(obj.scaleY))// // console.log( this.coordinateList[findIndex + 1].top, this.coordinateList[findIndex + 1].left,'----------item')// this.getSignatureJson();// }// });// // item.on('mouse:down', e => {// item.on("mouse:move", e => {// if(e.target!=null){// const obj =JSON.parse(JSON.stringify( e.target));// const top = obj.top;// const left = obj.left;// var findIndex= 0// this.coordinateList.forEach((element,index) => {// if(element.cacheKey == e.target.cacheKey){// findIndex = index// }// });// console.log( findIndex,this.coordinateList,e,'------------------ this.coordinateList[findIndex + 1]')// const keys = [// "width",// "height",// "top",// "left",// "angle",// "scaleX",// "scaleY"// ];// keys.forEach(item => {// // console.log(item, this.coordinateList[findIndex + 1][item],'-------------item')// this.coordinateList[findIndex][item] = Math.ceil(// obj[item] * obj["scaleX"]// );// });// // this.coordinateList[findIndex + 1].width= JSON.parse(JSON.stringify(obj.width))// // this.coordinateList[findIndex + 1].height= JSON.parse(JSON.stringify(obj.height))// if(this.coordinateList[findIndex].name == '批注'){// this.coordinateList[findIndex].xopinion= JSON.parse(JSON.stringify(top))// this.coordinateList[findIndex].yopinion= JSON.parse(JSON.stringify(left))// this.coordinateList[findIndex].fontsize= JSON.parse(JSON.stringify(obj.fontsize))// this.coordinateList[findIndex].opinion= JSON.parse(JSON.stringify(obj.text))// this.coordinateList[findIndex].wopinion= JSON.parse(JSON.stringify(obj.width))// this.coordinateList[findIndex].hopinion= JSON.parse(JSON.stringify(obj.height))// this.coordinateList[findIndex].opscale= JSON.parse(JSON.stringify(obj.scaleX))// }else{// this.coordinateList[findIndex].name = '印章'// this.coordinateList[findIndex].xsign= JSON.parse(JSON.stringify(top))// this.coordinateList[findIndex].ysign= JSON.parse(JSON.stringify(left))// this.coordinateList[findIndex].wsign= JSON.parse(JSON.stringify(obj.width))// this.coordinateList[findIndex].hsign= JSON.parse(JSON.stringify(obj.height))// this.coordinateList[findIndex].scale= JSON.parse(JSON.stringify(obj.scaleX))// }// this.coordinateList[findIndex ].scaleX= JSON.parse(JSON.stringify(obj.scaleX))// this.coordinateList[findIndex].scaleY= JSON.parse(JSON.stringify(obj.scaleY))// // console.log(obj.fontSize, top,left,'--obj.fontsize--------item')// console.log( this.coordinateList[findIndex ],'----------coordinate33List')// this.getSignatureJson();// }// });// });// },canvasEvents() {this.canvasEle.forEach(canvas => {canvas.on("object:moving", e => {const obj = e.target;if (!obj) return;// 边界处理obj.setCoords(); // 更新控制点坐标const canvasWidth = canvas.getWidth();const canvasHeight = canvas.getHeight();// 限制移动范围(考虑缩放)const boundingRect = obj.getBoundingRect();const scale = this.scale;// 计算有效移动范围const minX = 0 - (boundingRect.width * (1 - scale)) / 2;const minY = 0 - (boundingRect.height * (1 - scale)) / 2;const maxX = canvasWidth - (boundingRect.width * (1 + scale)) / 2;const maxY = canvasHeight - (boundingRect.height * (1 + scale)) / 2;// 应用位置限制obj.left = Math.min(Math.max(obj.left, minX), maxX);obj.top = Math.min(Math.max(obj.top, minY), maxY);// 更新坐标数据const findIndex = this.coordinateList.findIndex(item => item.cacheKey === obj.cacheKey);if (findIndex === -1) return;// 计算实际字体大小(核心修改部分)const baseFontSize = obj.baseFontSize || obj.fontSize; // 获取基准字号const scaleFactor = Math.max(obj.scaleX, obj.scaleY); // 取最大缩放值const actualFontSize = Math.round(baseFontSize * scaleFactor);// 通用属性更新const coordinate = this.coordinateList[findIndex];const updateProperty = (key, value) => {coordinate[key] = Math.round(value / this.scale);};updateProperty("left", obj.left);updateProperty("top", obj.top);updateProperty("width", obj.getScaledWidth());updateProperty("height", obj.getScaledHeight());coordinate.angle = Math.round(obj.angle);coordinate.scaleX = obj.scaleX;coordinate.scaleY = obj.scaleY;// 特定类型处理if (coordinate.name === "批注") {coordinate.xopinion = coordinate.top;coordinate.yopinion = coordinate.left;coordinate.opinion = obj.text;coordinate.fontsize = obj.fontSize;coordinate.wopinion = coordinate.width;coordinate.hopinion = coordinate.height;coordinate.opscale = obj.scaleX;coordinate.fontsize = actualFontSize;obj.set("fontSize", actualFontSize); // 更新画布显示字号// 重置缩放比例避免双重缩放obj.set({scaleX: 1,scaleY: 1,baseFontSize: actualFontSize // 更新基准字号});} else {coordinate.xsign = coordinate.top;coordinate.ysign = coordinate.left;coordinate.wsign = coordinate.width;coordinate.hsign = coordinate.height;coordinate.scale = obj.scaleX;}canvas.requestRenderAll();this.getSignatureJson();});canvas.on("mouse:move", e => {const obj = e.target;if (!obj) return;// 边界处理obj.setCoords(); // 更新控制点坐标const canvasWidth = canvas.getWidth();const canvasHeight = canvas.getHeight();// 限制移动范围(考虑缩放)const boundingRect = obj.getBoundingRect();const scale = this.scale;// 计算有效移动范围const minX = 0 - (boundingRect.width * (1 - scale)) / 2;const minY = 0 - (boundingRect.height * (1 - scale)) / 2;const maxX = canvasWidth - (boundingRect.width * (1 + scale)) / 2;const maxY = canvasHeight - (boundingRect.height * (1 + scale)) / 2;// 应用位置限制obj.left = Math.min(Math.max(obj.left, minX), maxX);obj.top = Math.min(Math.max(obj.top, minY), maxY);// 更新坐标数据const findIndex = this.coordinateList.findIndex(item => item.cacheKey === obj.cacheKey);if (findIndex === -1) return;// 计算实际字体大小(核心修改部分)const baseFontSize = obj.baseFontSize || obj.fontSize; // 获取基准字号const scaleFactor = Math.max(obj.scaleX, obj.scaleY); // 取最大缩放值const actualFontSize = Math.round(baseFontSize * scaleFactor);// 通用属性更新const coordinate = this.coordinateList[findIndex];const updateProperty = (key, value) => {coordinate[key] = Math.round(value / this.scale);};updateProperty("left", obj.left);updateProperty("top", obj.top);updateProperty("width", obj.getScaledWidth());updateProperty("height", obj.getScaledHeight());coordinate.angle = Math.round(obj.angle);coordinate.scaleX = obj.scaleX;coordinate.scaleY = obj.scaleY;// 特定类型处理if (coordinate.name === "批注") {coordinate.xopinion = coordinate.top;coordinate.yopinion = coordinate.left;coordinate.opinion = obj.text;coordinate.fontsize = obj.fontSize;coordinate.wopinion = coordinate.width;coordinate.hopinion = coordinate.height;coordinate.opscale = obj.scaleX;coordinate.fontsize = actualFontSize;obj.set("fontSize", actualFontSize); // 更新画布显示字号// 重置缩放比例避免双重缩放obj.set({scaleX: 1,scaleY: 1,baseFontSize: actualFontSize // 更新基准字号});} else {coordinate.xsign = coordinate.top;coordinate.ysign = coordinate.left;coordinate.wsign = coordinate.width;coordinate.hsign = coordinate.height;coordinate.scale = obj.scaleX;}canvas.requestRenderAll();this.getSignatureJson();});});},onMove(evt) {// 记录当前拖拽位置this.currentDragPosition = {x: evt.draggedContext.futureIndex,y: evt.draggedContext.index};console.log(this.currentDragPosition,"----------currentDragPosition----");},// 拖拽结束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;// 取整// console.log("e", e, findIndex);const left =e.originalEvent.offsetX < 0? 0: Math.ceil(e.originalEvent.offsetX / this.scale);const top =e.originalEvent.offsetY < 0? 0: Math.ceil(e.originalEvent.offsetY / this.scale);// console.log('e', e, findIndex,left,top,this.scale,);this.addSeal({sealUrl: this.mainImagelist[e.newDraggableIndex].img,left,top,index: e.newDraggableIndex,pageNum: findIndex});},// 注意图片的,onload是异步的,如果要封装成工具函数,需要用promise包装一下// 引入项目中的图片// 添加公章addSeal({ sealUrl, left, top, index, pageNum }) {const hasDuplicate = this.coordinateList.some(item => item.sealUrl === sealUrl && item.pageNo === pageNum + 1);if (hasDuplicate) {return this.$message.error("每页只能添加一个电子签名");}// 生成唯一 cacheKeyconst cacheKey = `seal_${Date.now()}_${pageNum}_${index}`;// const deleteIcon = "@assets/icon/del.svg";var deleteImg = document.createElement("img");deleteImg.src = this.deleteIcon;var samllImg = document.createElement("img");samllImg.src = this.samllIcon;var largeImg = document.createElement("img");largeImg.src = this.largeIcon;fabric.Image.fromURL(sealUrl, oImg => {oImg.set({left: left,top: top,cacheKey: cacheKey,cornerColor: "#3B82F6",cornerStyle: "circle",borderScaleFactor: 2,transparentCorners: false,// 角度// angle: 10,// 缩放比例,需要乘以scalescaleX: 1 * this.scale,scaleY: 1 * this.scale,// 设置当点击了该控制点,鼠标弹起是执行的动作处理方法index// 禁止缩放// lockScalingX: true,// lockScalingY: true,// 禁止旋转// lockRotation: true,});oImg.setControlsVisibility({mtr: false,mt: false,ml: false,mb: false,mr: false});oImg.controls.mtControl = new fabric.Control({visible: true, // 控制角的显隐x: -0.5,y: -0.5,offsetY: -16,offsetX: 30,cursorStyle: "pointer",//removeSignature// mouseDownHandler: (eventData, transform) => createLineDown(eventData, transform),mouseUpHandler: (eventData, transform) =>this.largeObject(eventData, transform),render: function(ctx, left, top, styleOverride, fabricObject) {// 渲染一个粉红色的正方形var size = this.cornerSize;ctx.save();ctx.translate(left, top);ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle));ctx.drawImage(largeImg, -size / 2, -size / 2, size, size);ctx.restore();},cornerSize: 26});// // 左上oImg.controls.tlControl = new fabric.Control({visible: true, // 控制角的显隐x: -0.5,y: -0.5,offsetY: -16,offsetX: 0,cursorStyle: "pointer",//removeSignature// mouseDownHandler: (eventData, transform) => createLineDown(eventData, transform),mouseUpHandler: (eventData, transform) =>this.smallObject(eventData, transform),render: function(ctx, left, top, styleOverride, fabricObject) {// 渲染一个粉红色的正方形var size = this.cornerSize;ctx.save();ctx.translate(left, top);ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle));ctx.drawImage(samllImg, -size / 2, -size / 2, size, size);ctx.restore();},cornerSize: 26});// 左上 删除oImg.controls.trControl = new fabric.Control({visible: true, // 控制角的显隐x: -0.5,y: -0.5,offsetY: -16,offsetX: 60,cursorStyle: "pointer",//removeSignature// mouseDownHandler: (eventData, transform) => createLineDown(eventData, transform),mouseUpHandler: (eventData, transform) =>this.deleteObject(eventData, transform),render: function(ctx, left, top, styleOverride, fabricObject) {// 渲染一个粉红色的正方形var size = this.cornerSize;ctx.save();ctx.translate(left, top);ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle));ctx.drawImage(deleteImg, -size / 2, -size / 2, size, size);ctx.restore();},cornerSize: 26});this.canvasEle[pageNum].add(oImg);// 保存签章信息this.saveSignature({ pageNum, index, sealUrl,cacheKey });});this.removeActive();},//控制器删除对象deleteObject(eventData, transform) {let target = transform.target;let canvas = target.canvas;// 拿到选中的文本的var activeObj = canvas.getActiveObject();const findIndex = this.coordinateList.findIndex(item =>item.cacheKey == activeObj.cacheKey && item.pageNo == activeObj.pageNo);// console.log(findIndex,this.coordinateList,activeObj,'-----------activeObjS')// 删除选中的文本canvas.remove(target); // 删除元素// 删除选中的文本的信息this.coordinateList.splice(findIndex, 1);this.getSignatureJson();},// 放大操作处理largeObject(eventData, transform) {this.handleScaling(transform, 0.1); // +10%缩放},// 缩小操作处理smallObject(eventData, transform) {this.handleScaling(transform, -0.1); // -10%缩放},// 统一缩放处理方法handleScaling(transform, scaleStep) {const target = transform.target;const canvas = target.canvas;const activeObj = canvas.getActiveObject();// 校验有效性if (!activeObj || !activeObj.cacheKey) {console.error("操作对象无效或缺少cacheKey");return;}console.log(this.coordinateList,'--------')// 精确查找坐标项const findIndex = this.coordinateList.findIndex(item => item.cacheKey === activeObj.cacheKey);if (findIndex === -1) {console.warn("未找到对应坐标项,cacheKey:", activeObj.cacheKey);return;}// 计算新缩放比例(带最小限制)const minScale = 0.2;const newScaleX = Math.max(minScale, activeObj.scaleX + scaleStep);const newScaleY = Math.max(minScale, activeObj.scaleY + scaleStep);// 更新对象属性activeObj.set({scaleX: newScaleX,scaleY: newScaleY}).setCoords();// 同步到数据存储this.coordinateList[findIndex] = {...this.coordinateList[findIndex],scaleX: newScaleX,scaleY: newScaleY,width: activeObj.width * newScaleX,height: activeObj.height * newScaleY};// 渲染优化(避免全局重绘)canvas.renderAll();console.log("缩放操作完成:", this.coordinateList[findIndex]);},// 保存签章saveSignature({ pageNum, index, sealUrl ,cacheKey}) {// 1. 拿到当前签章的信息let length = 0;let pageConfig = this.coordinateList.filter(item => item.pageNo - 1 == pageNum);if (pageConfig) length = pageConfig.length;const currentSignInfo = this.canvasEle[pageNum].getObjects()[length];// 2. 拼接数据const keys = ["width","height","top","left","angle","scaleX","scaleY","xsign",'cacheKey',"ysign"];const obj = {};keys.forEach(item => {obj[item] = Math.ceil(currentSignInfo[item] / this.scale);});obj.cacheKey = cacheKey;obj.sealUrl = sealUrl;obj.index = index;obj.xsign = obj.top;obj.ysign = obj.left;obj.scale = obj.scaleX;obj.wsign = obj.width;obj.hsign = obj.height;obj.name = "印章";obj.pageNo = pageNum + 1;// currentSignInfo.set("cacheKey", obj.cacheKey); // 关键:将 cacheKey 存入 Fabric 对象// currentSignInfo.set("name", obj.name); // 关键:将 cacheKey 存入 Fabric 对象this.coordinateList.push(obj);console.log(obj,'-------cacheKey')this.getSignatureJson();},// 签章生成json字符串getSignatureJson() {// 1. 判断是否有签章if (this.coordinateList.length <= 1) return (this.shadowInputValue = "");// 2. 拿到签章的信息,去除第一条const signatureList = this.coordinateList;// 3. 拼接数据,只要left和top和pageconst keys = ["pageNo","left","top","width","height","xsign","ysign","xopinion","text","yopinion"];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. 判断选中签章的个数是否大于1if (findItem.length > 1) {this.removeActive();return this.$message.error("只能选择删除一个签章,请重新选择");}// 4. 拿到选中的签章的cacheKeyconst activeObj = findItem[0].getActiveObject();const findIndex = this.coordinateList.findIndex(item =>item.cacheKey == activeObj.cacheKey && item.pageNo == activeObj.pageNo);// 5. 删除选中的签章findItem[0].remove(activeObj);// 6. 删除选中的签章的信息this.coordinateList.splice(findIndex, 1);this.getSignatureJson();},// 清空签章clearSignature() {this.canvasEle.forEach(item => {item.clear();});this.coordinateList = [];this.getSignatureJson();},processSignsData(originalData) {// 按pageNo分组const grouped = originalData.reduce((acc, item) => {const pageNo = item.pageNo;if (!acc[pageNo]) acc[pageNo] = [];acc[pageNo].push(item);return acc;}, {});// 生成目标数据格式const signs = Object.keys(grouped).map(Number).sort((a, b) => a - b).map(pageNo => {const items = grouped[pageNo];const signItem = {xsign: "",ysign: "",wsign: "",hsign: "",xopinion: "",yopinion: "",wopinion: "",hopinion: "",scale: "1",opscale: "1",sealUrl: "",opinion: "",pageNo: "",fontsize: ""};items.forEach(item => {if (item.name === "印章") {signItem.xsign = String(item.xsign ?? "");signItem.ysign = String(item.ysign ?? "");signItem.wsign = String(item.wsign ?? "");signItem.hsign = String(item.hsign ?? "");signItem.scale = String(item.scaleX ?? "");signItem.sealUrl = String(item.sealUrl ?? "");} else if (item.name === "批注") {signItem.xopinion = String(item.xopinion ?? "");signItem.yopinion = String(item.yopinion ?? "");signItem.wopinion = String(item.wopinion ?? "");signItem.hopinion = String(item.hopinion ?? "");signItem.opscale = String(item.scaleX ?? "");signItem.opinion = String(item.opinion ?? "");signItem.fontsize = String(item.fontsize ?? "");}signItem.pageNo = String(item.pageNo ?? "");});return signItem;});return { signs };},//操作单据optionSignature(val) {this.id = this.$route.query.id;var includesign = this.coordinateList.some(item =>item.sealUrl?.includes("/chairmansoffice/download/"));console.log(this.coordinateList,"------------------------this.coordinateList ");if (val == "agree" ||val == "addSubSign") {if (includesign &&this.coordinateList != [] &&this.coordinateList.length > 0) {this.isVisible = true;this.signlist = this.processSignsData(this.coordinateList)["signs"];// console.log(// this.signlist,// "------------------------this.coordinateList "// );} else {this.$message.warning(`请先对文件进行签章后再提交`);}this.title = val;} else {this.title = "return";this.isVisible = true;}// console.log("this.coordinateList", this.coordinateList);},closeVisible(value) {this.isVisible = value;},//返回returnSignature() {this.$router.go(-1);},// 提交数据submitSignature() {console.log("this.coordinateList", this.coordinateList);},//添加文字addTextobjectHandle(e) {var deleteImg = document.createElement("img");deleteImg.src = this.deleteIcon;var samllImg = document.createElement("img");samllImg.src = this.samllIcon;var largeImg = document.createElement("img");largeImg.src = this.largeIcon;let Shape;let currentoptionCss;var findIndex = this.pageNum - 1;const cacheKey = `text_${Date.now()}_${findIndex}`;currentoptionCss = this.activeobjectData;//通过最大行高计算高度,并删除多余文字,多出文字..表示,三个会换行Shape = new fabric.IText(currentoptionCss.text || "", currentoptionCss);Shape.set({cacheKey: cacheKey // 为文本对象设置唯一标识});Shape.setControlVisible("mtr", false);Shape.setControlVisible("mt", false);Shape.setControlVisible("ml", false);Shape.setControlVisible("mb", false);Shape.setControlVisible("mr", false);Shape.controls.mtControl = new fabric.Control({visible: true, // 控制角的显隐x: -0.5,y: -0.5,offsetY: -16,offsetX: 30,cursorStyle: "pointer",//removeSignature// mouseDownHandler: (eventData, transform) => createLineDown(eventData, transform),mouseUpHandler: (eventData, transform) =>this.largeObject(eventData, transform),render: function(ctx, left, top, styleOverride, fabricObject) {var size = this.cornerSize;ctx.save();ctx.translate(left, top);ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle));ctx.drawImage(largeImg, -size / 2, -size / 2, size, size);ctx.restore();},cornerSize: 26});Shape.controls.tlControl = new fabric.Control({visible: true, // 控制角的显隐x: -0.5,y: -0.5,offsetY: -16,offsetX: 0,cursorStyle: "pointer",//removeSignature// mouseDownHandler: (eventData, transform) => createLineDown(eventData, transform),mouseUpHandler: (eventData, transform) =>this.smallObject(eventData, transform),render: function(ctx, left, top, styleOverride, fabricObject) {var size = this.cornerSize;ctx.save();ctx.translate(left, top);ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle));ctx.drawImage(samllImg, -size / 2, -size / 2, size, size);ctx.restore();},cornerSize: 26});// 左上 删除Shape.controls.trControl = new fabric.Control({visible: true, // 控制角的显隐x: -0.5,y: -0.5,offsetY: -16,offsetX: 60,cursorStyle: "pointer",//removeSignature// mouseDownHandler: (eventData, transform) => createLineDown(eventData, transform),mouseUpHandler: (eventData, transform) =>this.deleteObject(eventData, transform),render: function(ctx, left, top, styleOverride, fabricObject) {// 渲染一个粉红色的正方形var size = this.cornerSize;ctx.save();ctx.translate(left, top);ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle));ctx.drawImage(deleteImg, -size / 2, -size / 2, size, size);ctx.restore();},cornerSize: 26});Shape.splitByGrapheme = true;var index = 0,index = index + 1;var pageNum = findIndex;var sealUrl = this.activeobjectData.text;// 获取当前页面信息const currentPage = this.pageNum;const canvasIndex = currentPage - 1;// 验证当前页是否已有批注const hasExistingAnnotation = this.coordinateList.some(item => item.pageNo === currentPage && item.name === "批注");if (hasExistingAnnotation) {this.$message.error("每页只能添加一个文字批注");return;}this.canvasEle[findIndex].add(Shape).setActiveObject(Shape);this.saveText({ pageNum, index, sealUrl,cacheKey });},saveText({ pageNum, index, sealUrl,cacheKey }) {// 1. 拿到当前批注的信息let length = 0;let pageConfig = this.coordinateList.filter(item => item.pageNo - 1 == pageNum);if (pageConfig) length = pageConfig.length;const currentSignInfo = this.canvasEle[pageNum].getObjects()[length];const keys = ["width","height","cacheKey","top","left","angle","scaleX","scaleY"];const obj = {};keys.forEach(item => {obj[item] = Math.ceil(currentSignInfo[item] / this.scale);});obj.cacheKey = cacheKey;obj.opinion = obj.text;obj.fontsize = obj.fontsize;obj.xopinion = obj.top;obj.yopinion = obj.left;obj.wopinion = obj.width;obj.hopinion = obj.height;obj.opscale = obj.scaleX;obj.index = index;obj.name = "批注";obj.pageNo = pageNum + 1;// currentSignInfo.set("cacheKey", obj.cacheKey); // 关键:将 cacheKey 存入 Fabric 对象// currentSignInfo.set("name", obj.name); // 关键:将 cacheKey 存入 Fabric 对象this.coordinateList.push(obj);},//下载文件download() {// downloadApi(this.id)// .then(res => {// const fileNameEncode = res.headers["content-disposition"]// .split(";")[1]// .split("filename=")[1];// let contentDisposition = decodeURIComponent(fileNameEncode);// this.downloadBinaryFile(res.data, contentDisposition);// this.loading = false;// })// .catch();},async downloadBinaryFile(binFile,fileName,blobType = "application/octet-stream") {// 处理二进制数据并创建 Blob 对象const blobObj = new Blob([binFile], { type: blobType });// 创建一个链接并设置下载属性const downloadLink = document.createElement("a");let url = window.URL || window.webkitURL || window.moxURL; // 兼容不同浏览器的 URL 对象url = url.createObjectURL(blobObj);downloadLink.href = url;downloadLink.download = fileName; // 设置下载的文件名// 将链接添加到 DOM 中,模拟点击document.body.appendChild(downloadLink);downloadLink.click();// 移除创建的链接和释放 URL 对象document.body.removeChild(downloadLink);window.URL.revokeObjectURL(url);},customRequest(param) {this.uploadAction(param.file);},//上传文件async uploadAction(file) {this.dayLoading = true;const param = new FormData();param.append("file", file);uploadSignture(param).then(res => {if (res.data.type == "application/json") {//转换步骤const file = new FileReader();file.readAsText(res.data, "utf-8");file.onload = e => {const obj = JSON.parse(file.result);res.data = obj;if (obj.code == "100") {this.$message({ message: "上传成功", type: "success" });this.userInfo.signture = obj.data.fn;// this.form.fn = obj.data[0].fn;this.getsignimg();// this.form.sourceName = obj.data[0].sourceName;// console.log(this.userInfo.signture);} else {this.$message({ message: obj.message, type: "error" });}};} else {this.$message.warning(`上传失败`);}}).catch(err => {this.$message.error(res.$message);});// this.dialogVisible = false;// this.$emit("dialogVisible", this.dialogVisible);},// 超出上传文件个数的钩子handleExceed(file, fileList) {this.$message.warning(`只能上传一个文件`);},beforeRemove(file, fileList) {return this.$confirm(`确定移除 ${file.name}?`);},handleRemove(file, fileList) {console.log(file, fileList);},handlePreview(file) {console.log(file);},//删除绑定个人的签章文件deleteSignature() {removeSignture().then(res => {if (res.data.code == 100) {this.$message({ message: res.data.message, type: "success" });this.getsignimg();this.vissign = false;} else {this.$message({ message: res.data.message, type: "error" });}});}// imgsrc(val){// let token = getToken()// Object.defineProperty(Image.prototype, 'authsrc', {// writable: true,// enumerable: true,// configurable: true// })// let img = val// let request = new XMLHttpRequest();// request.responseType = 'blob';// request.open('get', this.authSrc, true);// request.setRequestHeader('token', token);// request.onreadystatechange = e => {// if (request.readyState == XMLHttpRequest.DONE && request.status == 200) {// img.src = URL.createObjectURL(request.response);// img.onload = () => {// URL.revokeObjectURL(img.src);// }// }// };// request.send(null);// },}
};
</script>
<style lang="scss" scoped>
.contract-signature-view {/*pdf部分*/.ele-canvas {overflow: hidden;}.text-canvas {overflow: hidden;}.title-operation {background: rgba(255, 255, 255, 1);height: 80px;padding: 20px 40px;display: flex;align-items: center;justify-content: space-between;.operation {.searchbutton {margin-right: 10px;min-width: 96px;height: 40px;border: 1px 0px 0px 0px;gap: 8px;border-radius: 4px;padding-top: 8px;padding-right: 20px;padding-bottom: 8px;padding-left: 18px;font-size: 12px;}.button {margin-right: 10px;min-width: 96px;height: 40px;border: none;gap: 8px;border-radius: 4px;padding-top: 8px;padding-right: 20px;padding-bottom: 8px;padding-left: 18px;font-size: 12px;}}.title {font-size: 20px;font-weight: 600;}border-bottom: 1px solid #e4e4e4;}.section-box {position: relative;display: flex;height: calc(100vh - 60px);.main-layout {// flex: 1;width: 100%;background-color: #f7f8fa;position: relative;&.is-first {.operate-box {opacity: 0;}}.operate-box {opacity: 1;position: absolute;top: 80px;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;}}.pageNo-change {display: flex;align-items: center;margin-left: 30px;.icon {cursor: pointer;padding: 0 5px;color: #c1c1c1;}.input-box {border: none;.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 - 185px);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;#box {position: relative;}.menu-x {visibility: hidden;z-index: -100;position: absolute;top: 0;left: 0;box-sizing: border-box;border-radius: 4px;box-shadow: 0 0 4px rgba(0, 0, 0, 0.3);background-color: #fff;}.menu-li {box-sizing: border-box;padding: 4px 8px;border-bottom: 1px solid #ccc;cursor: pointer;}.menu-li:hover {background-color: antiquewhite;}.menu-li:first-child {border-top-left-radius: 4px;border-top-right-radius: 4px;}.menu-li:last-child {border-bottom: none;border-bottom-left-radius: 4px;border-bottom-right-radius: 4px;}}}.loading {width: 20px;height: 20px;position: absolute;top: 50%;left: 50%;transform: translate(-50%, -50%);z-index: 999;.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;}}}}}
}.signatureimg {min-width: 240px !important;background-color: #fff;//padding-bottom: 20px;.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 {// margin: 10p;// padding: 10px;border: 1px dashed rgba(0, 0, 0, 0.3);&:not(:last-child) {margin-bottom: 10px;}.img {vertical-align: middle;background-repeat: no-repeat;}}.opera {bottom: 0;position: absolute;justify-content: space-between;background: rgba(243, 243, 243, 1);left: 0;width: 100%;display: flex;align-items: center;.delbut {cursor: pointer;background: rgba(243, 243, 243, 1);border: none;color: #000000;}:hover {color: #409eff;border: #c6e2ff;// background-color: #ecf5ff;}}
}.upimg {.upload-demo {padding: 10px 20px;text-align: center;.upload-button {padding: 30px 0 0 0;color: #3f7afa;font-size: 14px;border: none;&:hover,&.is-active {background: transparent;}}}
}
</style>