合同签章及批注模块涉及以下核心难点和复杂点,按功能模块归类如下:
效果展示
使用拖拽的方式将签名放入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-button
v-if="$route.query.formst==31"
class="searchbutton"
type="primary"
@click="optionSignature('addSubSign')"
>
<svg-icon icon-class="check" />呈签</el-button
>
<el-button
v-else
class="searchbutton"
type="primary"
@click="optionSignature('agree')"
>
<svg-icon icon-class="check" />同意</el-button
>
<!-- <el-button
class="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">
<!-- 拖拽 -->
<draggable
v-model="mainImagelist"
:group="{ name: 'itext', pull: 'clone' }"
:sort="false"
@end="end"
@move="onMove"
>
<transition-group type="transition">
<li
v-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-button
class="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-upload
class="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>
<div
slot="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-input
class="input-box"
v-model.number="pageNum"
:max="defaultNumPages"
@change="cutover"
/>
<span class="default-text">/{{ defaultNumPages }}</span>
<i class="icon el-icon-arrow-right" @click="nextPage" />
</div>
</div>
<!-- <div class="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-view
outViewDom: null,
// 各页pdf的canvas-layout
canvasLayoutTopList: [],
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,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",
samllIcon:"data:image/png;base64,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",
largeIcon:"data:image/png;base64,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",
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.png
if (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"));
});
// 循环渲染pdf
for (let i = 1; i <= this.numPages; i++) {
this.renderPage(i).then(() => {
this.renderPdf({
width: this.canvas[i - 1].width,
height: this.canvas[i - 1].height
});
});
}
setTimeout(() => {
this.renderFabric();
this.canvasEvents();
}, 1000);
});
});
},
// 设置pdf宽高,缩放比例,渲染pdf
renderPage(num) {
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 context
const 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-layout
const 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. 拿到签名canvas
const canvasEle = item.querySelector(".ele-canvas");
// 5. 拿到pdf的canvas
const pCenter = item.querySelector(".the-canvas");
// 6. 设置签名canvas的宽高
canvasEle.width = pCenter.clientWidth;
canvasEle.height = this.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滑块,重新渲染整个pdf
sliderChange() {
this.pageNum = 1;
this.numPages = 0;
this.canvasLayoutTopList = [];
this.canvasEle = [];
this.ctx = [];
this.canvas = [];
this.isShowPdf = false;
// this.outViewScrollClose();
this.whDatas = null;
this.coordinateList = [];
this.getSignatureJson();
setTimeout(() => {
this.numPages = this.pdfDoc.numPages;
this.$nextTick(() => {
this.canvas = document.querySelectorAll(".the-canvas");
this.canvas.forEach(item => {
this.ctx.push(item.getContext("2d"));
});
// 循环渲染pdf
for (let i = 1; i <= this.numPages; i++) {
this.renderPage(i).then(() => {
this.renderPdf({
width: this.canvas[i - 1].width,
height: this.canvas[i - 1].height
});
});
}
setTimeout(() => {
this.renderFabric();
this.canvasEvents();
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("每页只能添加一个电子签名");
}
// 生成唯一 cacheKey
const 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,
// 缩放比例,需要乘以scale
scaleX: 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和page
const 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. 判断选中签章的个数是否大于1
if (findItem.length > 1) {
this.removeActive();
return this.$message.error("只能选择删除一个签章,请重新选择");
}
// 4. 拿到选中的签章的cacheKey
const activeObj = findItem[0].getActiveObject();
const findIndex = this.coordinateList.findIndex(
item =>
item.cacheKey == activeObj.cacheKey && 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>