前端实现签字效果+合同展示
PBitW 2024-08-11 08:03:01 阅读 50
文章目录
获取一个高度会变的元素的高度获取元素设置的 transform适配手机transform-origin: 5% 0; 的原因修改后
签字效果取消el-dialog的头部+边距为什么禁止界面滚动vue3 使用 nextTick实现效果
签字判断是横是竖canvas 去掉空白部分canvas裁剪图片最终完善代码,可以直接使用
最近菜鸟公司要做一个这样的功能,后端返回一个合同的整体html,前端进行签字,以下是一些重要思路!
注:本文章是给自己看的,读者酌情考虑!
获取一个高度会变的元素的高度
script 代码
<code>let bigBoxHeight = ref(0);
// 获取到元素
let bigBox = document.querySelector(".bigBox");
// 设置高度为 auto
bigBox.style.height = "auto";
// 获取 offsetHeight
const height = bigBox.offsetHeight;
// 设置值
bigBoxHeight.value = height;
注:
offsetHeight:返回一个元素的高度,包括其padding和border,但不包括其margin。
template 代码
<div class="bigBox" :style="{ height: bigBoxHeight + 'px' }">code>
<div class="contractBox">code>
<div v-html="printData"></div>code>
</div>
<!-- 遮罩层,返回的printData里设置了可编辑,但是这里只是展示用,且修改了也不会有影响,所以就简单的加个遮罩就行了 -->
<div class="markBox" :style="{ height: bigBoxHeight + 'px' }"></div>code>
</div>
获取元素设置的 transform
感谢:原生js获取元素transform的scale和rotate
// 获取设置了transform的元素
let contractBox = document.querySelector(".contractBox");
// 获取浏览器计算后的结果
let st = window.getComputedStyle(contractBox, null);
// 从结算后的结果中找到 transform,也可以直接 st.transform
var tr = st.getPropertyValue("transform");
if (tr === "none") { -- -->
// 为none表示未设置
bigBox.style.height = "auto";
const height = bigBox.offsetHeight + 50;
bigBoxHeight.value = height;
} else {
bigBox.style.height = "auto";
// 缩放需要 * 缩放比例 + 边距(margin/padding)
const height = bigBox.offsetHeight * 0.5 + 50;
bigBoxHeight.value = height;
}
getComputedStyle 可以学习我的博客:看 Javascript实战详解 收获一
适配手机
上面设置transform是因为返回的html文档不是那么的自适应,所以菜鸟就在手机端,让其渲染700px,但是再缩小0.5倍去展示,即可解决!
css 代码
@media screen and (max-width: 690px) {
.contractBox {
width: 700px !important;
transform: scale(0.5);
// 防止延中心点缩放而导致上面留白很多(合同很长,7000px左右)
transform-origin: 5% 0;
}
}
.bigBox {
position: relative;
// 设置是因为 scale 缩放了但是元素还是占本身那么大,所以要超出隐藏
overflow: hidden;
.markBox {
width: 100%;
position: absolute;
left: 0;
bottom: 0;
top: 0;
bottom: 0;
}
}
.contractBox {
width: 70%;
margin: 50px auto 0px;
overflow: hidden;
}
transform-origin: 5% 0; 的原因
这里设置 5% 是为了居中,因为这里有个问题就是不能设置bigBox为display:flex,不然里面的内容就是按照width:100%然后缩放0.5,而不是width:700px来缩放的!
是flex搞的鬼,菜鸟这里就用了个简单办法。
其实正统做法应该是获取宽度,再用窗口宽度减去获取的宽度 / 2,然后通过该值设置margin!
修改后
菜鸟既然想到了上面的居中方式,那就直接实现了,这里给上代码!
script 代码
// 是否缩放,来确定margin-left取值
let isScale = ref(false);
let bigBoxmargin = ref(0);
let bigBox = document.querySelector(".bigBox");
let contractBox = document.querySelector(".contractBox");
let st = window.getComputedStyle(contractBox, null);
var tr = st.getPropertyValue("transform");
if (tr === "none") {
isScale.value = false;
bigBox.style.height = "auto";
const height = bigBox.offsetHeight + 50;
bigBoxHeight.value = height;
} else {
isScale.value = true;
bigBox.style.height = "auto";
// 缩放需要 * 缩放比例 + 边距(margin/padding)
const height = bigBox.offsetHeight * 0.5 + 50;
// 不用 st.witdh 是因为 st.witdh 获取的值是 700px,不能直接运算,这里菜鸟就偷懒了,不想处理了
bigBoxmargin.value = (window.innerWidth - 700 * 0.5) / 2;
bigBoxHeight.value = height;
}
template 代码
<div class="bigBox" :style="{ height: bigBoxHeight + 'px' }">code>
<div class="contractBox" :style="{ marginLeft: isScale ? bigBoxmargin + 'px' : 'auto' }">code>
<div v-html="printData"></div>code>
</div>
<div class="markBox" :style="{ height: bigBoxHeight + 'px' }"></div>code>
</div>
签字效果
这里签字效果,菜鸟是使用 el-dialog 实现的,el-dialog 的使用方式见:element plus 使用细节
这里主要粘贴签字的代码
<script setup>
import { -- --> ref, onMounted, nextTick } from "vue";
// eslint-disable-next-line
const props = defineProps({
dialogVisible: {
type: Boolean,
default: false,
},
});
// eslint-disable-next-line
const emit = defineEmits(["closeEvent"]);
// 关闭弹窗
function handleClose() {
emit("closeEvent", false);
// 解除禁止页面滚动
document.body.removeEventListener("touchmove", preventDefault);
}
const dialogBox = ref();
function closeDialog() {
dialogBox.value.resetFields();
}
// 禁止页面滚动
function preventDefault(e) {
e.preventDefault();
}
document.body.addEventListener("touchmove", preventDefault, { passive: false });
// 签名
// 配置内容
const config = {
width: window.innerWidth, // 宽度
height: window.innerHeight - 300, // 高度,减300是为了给dialog的footer一点空间显示
lineWidth: 5, // 线宽
strokeStyle: "red", // 线条颜色
lineCap: "round", // 设置线条两端圆角
lineJoin: "round", // 线条交汇处圆角
};
let canvas = null;
let ctx = null;
onMounted(async () => {
await nextTick();
// 获取canvas 实例
canvas = document.querySelector(".canvas");
// 设置宽高
canvas.width = config.width;
canvas.height = config.height;
// 设置一个边框
canvas.style.border = "1px solid #000";
// 创建上下文
ctx = canvas.getContext("2d");
// 设置填充背景色
ctx.fillStyle = "transparent";
// 绘制填充矩形
ctx.fillRect(
0, // x 轴起始绘制位置
0, // y 轴起始绘制位置
config.width, // 宽度
config.height // 高度
);
});
// 保存上次绘制的 坐标及偏移量
const client = {
offsetX: 0, // 偏移量
offsetY: 0,
endX: 0, // 坐标
endY: 0,
};
// 判断是否为移动端
const mobileStatus = /Mobile|Android|iPhone/i.test(navigator.userAgent);
// 初始化
const init = (event) => {
// 获取偏移量及坐标
const { offsetX, offsetY, pageX, pageY } = mobileStatus ? event.changedTouches[0] : event;
// 修改上次的偏移量及坐标
client.offsetX = offsetX;
client.offsetY = offsetY;
client.endX = pageX;
client.endY = pageY;
// 清除以上一次 beginPath 之后的所有路径,进行绘制
ctx.beginPath();
// 根据配置文件设置相应配置
ctx.lineWidth = config.lineWidth;
ctx.strokeStyle = config.strokeStyle;
ctx.lineCap = config.lineCap;
ctx.lineJoin = config.lineJoin;
// 设置画线起始点位
ctx.moveTo(client.endX, client.endY);
// 监听 鼠标移动或手势移动
window.addEventListener(mobileStatus ? "touchmove" : "mousemove", draw);
};
// 绘制
const draw = (event) => {
console.log(event);
// 获取当前坐标点位
const { pageX, pageY } = mobileStatus ? event.changedTouches[0] : event;
// 超出范围不监听
if (pageY > config.height) {
return;
}
// 修改最后一次绘制的坐标点
client.endX = pageX;
client.endY = pageY;
// 根据坐标点位移动添加线条
ctx.lineTo(pageX, pageY);
// 绘制
ctx.stroke();
};
// 结束绘制
const cloaseDraw = () => {
// 结束绘制
ctx.closePath();
// 移除鼠标移动或手势移动监听器
window.removeEventListener("mousemove", draw);
};
// 创建鼠标/手势按下监听器
window.addEventListener(mobileStatus ? "touchstart" : "mousedown", init);
// 创建鼠标/手势 弹起/离开 监听器
window.addEventListener(mobileStatus ? "touchend" : "mouseup", cloaseDraw);
// 取消-清空画布
const cancel = () => {
// 清空当前画布上的所有绘制内容
ctx.clearRect(0, 0, config.width, config.height);
};
// 保存-将画布内容保存为图片
const save = () => {
// 将canvas上的内容转成blob流
canvas.toBlob((blob) => {
// 获取当前时间并转成字符串,用来当做文件名
const date = Date.now().toString();
// 创建一个 a 标签
const a = document.createElement("a");
// 设置 a 标签的下载文件名
a.download = `${ date}.png`;
// 设置 a 标签的跳转路径为 文件流地址
a.href = URL.createObjectURL(blob);
// 手动触发 a 标签的点击事件
a.click();
// 移除 a 标签
a.remove();
});
handleClose();
};
</script>
<template>
<div>
<el-dialog
title="签字"code>
ref="dialogBox"code>
:modelValue="dialogVisible"code>
:before-close="handleClose"code>
@close="closeDialog"code>
:close-on-click-modal="false"code>
:destroy-on-close="true"code>
top="0"code>
width="100%"code>
>
<canvas class="canvas"></canvas>code>
<template #footer>
<div>
<el-button type="primary" @click="save">保存</el-button>code>
<el-button @click="cancel">清除</el-button>code>
<el-button @click="handleClose">关闭</el-button>code>
</div>
</template>
</el-dialog>
</div>
</template>
<style lang="scss">code>
.el-dialog__header { -- -->
display: none;
}
.el-dialog__body {
padding: 0 !important;
}
</style>
取消el-dialog的头部+边距
因为这里的 client 设置的偏移量都是 0,菜鸟不会改(感觉应该加上el-dialog的头部+边框的偏移量),如果不取消的话,就是错位着写的!
为什么禁止界面滚动
这里禁止是因为手机端,签名时写 “竖” 操作时,容易触发下拉整个界面的事件!导致写字中断,体验感极差,所以弹窗弹出时阻止事件,关闭后移除!
这里函数 preventDefault 必须提出,不然会取消不掉!
vue3 使用 nextTick
获取元素必须在 onMounted 中,但是 el-dialog 即使写在 onMounted 里面也不行,需要加上 nextTick !
实现效果
签字判断是横是竖
今天菜鸟又遇见了大麻烦,就是这个签字不能知道别人是横屏横着写的还是竖屏横着写的,eg:
菜鸟的思路就是获取到签字部分,然后如果横着签字就直接截取那部分设置样式,如果竖着签字就设置样式旋转 -90deg,那如何获取签字部分的大小呢?
canvas 去掉空白部分
canvas 去掉空白部分
修改上面 save 中代码
<code>const save = () => { -- -->
// 将canvas上的内容转成blob流
var imgData = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
var lOffset = canvas.width,
rOffset = 0,
tOffset = canvas.height,
bOffset = 0;
for (var i = 0; i < canvas.width; i++) {
for (var j = 0; j < canvas.height; j++) {
var pos = (i + canvas.width * j) * 4;
if (imgData[pos] > 0 || imgData[pos + 1] > 0 || imgData[pos + 2] || imgData[pos + 3] > 0) {
// 说第j行第i列的像素不是透明的
// 楼主貌似底图是有背景色的,所以具体判断RGBA的值可以根据是否等于背景色的值来判断
bOffset = Math.max(j, bOffset); // 找到有色彩的最底部的纵坐标
rOffset = Math.max(i, rOffset); // 找到有色彩的最右端
tOffset = Math.min(j, tOffset); // 找到有色彩的最上端
lOffset = Math.min(i, lOffset); // 找到有色彩的最左端
}
}
}
}
canvas.getContext(‘2d’).getImageData(0, 0, 宽, 高) 会返回一个当前 canvas 的图像数据对象,其中有一个data属性,是一个一维数组,这个一维数组,每4个下标分别代表了一个像素点的 R,G,B,A 的值,只需要遍历这些值就能找到边界了。
感谢:canvas 裁剪空白区域
canvas裁剪图片
canvas裁剪图片
但是获取到了区域并不行,因为我还需要将其截取,然后转成图片传给后端,且还要让后端知道到底是横着放 html 模板里还是竖着放,思来想去,感觉直接返回 base64 的 img 元素给后端更好,因为我就可以直接设置style,后端只需要放到对应的地方就行,所以save继续修改为
const save = () => {
// 将canvas上的内容转成blob流
var imgData = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
var lOffset = canvas.width,
rOffset = 0,
tOffset = canvas.height,
bOffset = 0;
for (var i = 0; i < canvas.width; i++) {
for (var j = 0; j < canvas.height; j++) {
var pos = (i + canvas.width * j) * 4;
if (imgData[pos] > 0 || imgData[pos + 1] > 0 || imgData[pos + 2] || imgData[pos + 3] > 0) {
// 说第j行第i列的像素不是透明的
// 楼主貌似底图是有背景色的,所以具体判断RGBA的值可以根据是否等于背景色的值来判断
bOffset = Math.max(j, bOffset); // 找到有色彩的最底部的纵坐标
rOffset = Math.max(i, rOffset); // 找到有色彩的最右端
tOffset = Math.min(j, tOffset); // 找到有色彩的最上端
lOffset = Math.min(i, lOffset); // 找到有色彩的最左端
}
}
}
// 重新创建一个canvas,将之前的canvas上的图片,按照获取到的大小去截取
const trimmedWidth = rOffset - lOffset + 1;
const trimmedHeight = bOffset - tOffset + 1;
const trimmedCanvas = document.createElement("canvas");
trimmedCanvas.width = trimmedWidth;
trimmedCanvas.height = trimmedHeight;
const trimmedContext = trimmedCanvas.getContext("2d");
trimmedContext.putImageData(
ctx.getImageData(lOffset, tOffset, trimmedWidth, trimmedHeight),
0,
0
);
// 将截取后的生成图片,并设置样式
console.log(trimmedWidth);
console.log(trimmedHeight);
var newUrl = trimmedCanvas.toDataURL();
var newImage = new Image();
newImage.src = newUrl;
console.log(trimmedWidth < trimmedHeight);
if (trimmedWidth < trimmedHeight) {
newImage.style.height = "100px";
newImage.style.transform = "rotate(-" + 90 + "deg)";
} else {
newImage.style.width = "100px";
}
console.log(newImage);
handleClose();
};
newImage 打印出来是一个元素:
<img src="data:image/png;base64,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" style="transform: rotate(-90deg);">code>
至此算是完成了整个签字功能!
最终完善代码,可以直接使用
其实这里还有一个问题,就是不知道横屏后的用户到底是哪边横屏,可能要旋转-90deg,也可能是正90deg,这里菜鸟想的是一个简单办法,就是给个示例文字,让用户根据示例文字进行签字!
template
<template>
<div>
<el-dialog
title="签字"code>
ref="dialogBox"code>
:modelValue="dialogVisible"code>
:before-close="handleClose"code>
@close="closeDialog"code>
:close-on-click-modal="false"code>
:destroy-on-close="true"code>
top="0"code>
width="100%"code>
>
<div class="canvasBox" :style="{ height: config.height + 'px' }">code>
<canvas class="canvas"></canvas>code>
<div class="tipTxt" v-show="showTip">code>
<p>示例文字!</p>
<p class="redTxt">请按示例文字方向,正楷清晰书写。谢谢!</p>code>
</div>
</div>
<template #footer>
<div>
<el-button type="primary" @click="save">保存</el-button>code>
<el-button @click="cancel">清除</el-button>code>
<el-button @click="handleClose(false)">关闭</el-button>code>
</div>
</template>
</el-dialog>
</div>
</template>
<style lang="scss">code>
.el-dialog__header { -- -->
display: none;
}
.el-dialog__body {
padding: 0 !important;
}
.canvasBox {
position: relative;
}
.canvas,
.tipTxt {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
}
.tipTxt {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.tipTxt p {
color: #999;
font-size: 80px;
}
.tipTxt .redTxt {
color: rgba(255, 0, 0, 0.5);
font-size: 50px;
}
@media screen and (max-width: 690px) {
.tipTxt {
transform: rotate(90deg);
}
.tipTxt p {
font-size: 50px;
}
.tipTxt .redTxt {
font-size: 18px;
}
}
</style>
js 控制 showTip 的展示
<script setup>
import { ref, onMounted, nextTick } from "vue";
// eslint-disable-next-line
const props = defineProps({
dialogVisible: {
type: Boolean,
default: false,
},
});
// eslint-disable-next-line
const emit = defineEmits(["closeEvent"]);
// 关闭弹窗
function handleClose(imgEl) {
console.log(imgEl);
emit("closeEvent", imgEl);
// 禁止页面滚动
document.body.removeEventListener("touchmove", preventDefault);
}
const dialogBox = ref();
function closeDialog() {
dialogBox.value.resetFields();
}
// 禁止页面滚动
function preventDefault(e) {
e.preventDefault();
}
document.body.addEventListener("touchmove", preventDefault, { passive: false });
// 签名
// 配置内容
const config = {
width: window.innerWidth, // 宽度
height: window.innerHeight - 150, // 高度
lineWidth: 5, // 线宽
strokeStyle: "red", // 线条颜色
lineCap: "round", // 设置线条两端圆角
lineJoin: "round", // 线条交汇处圆角
};
let canvas = null;
let ctx = null;
onMounted(async () => {
await nextTick();
// 获取canvas 实例
canvas = document.querySelector(".canvas");
console.log(canvas);
// 设置宽高
canvas.width = config.width;
canvas.height = config.height;
// 设置一个边框
canvas.style.border = "1px solid #000";
// 创建上下文
ctx = canvas.getContext("2d");
// 设置填充背景色
ctx.fillStyle = "transparent";
// 绘制填充矩形
ctx.fillRect(
0, // x 轴起始绘制位置
0, // y 轴起始绘制位置
config.width, // 宽度
config.height // 高度
);
});
// 保存上次绘制的 坐标及偏移量
const client = {
offsetX: 0, // 偏移量
offsetY: 0,
endX: 0, // 坐标
endY: 0,
};
// 判断是否为移动端
const mobileStatus = /Mobile|Android|iPhone/i.test(navigator.userAgent);
// 初始化
const init = (event) => {
// 获取偏移量及坐标
const { offsetX, offsetY, pageX, pageY } = mobileStatus ? event.changedTouches[0] : event;
// 修改上次的偏移量及坐标
client.offsetX = offsetX;
client.offsetY = offsetY;
client.endX = pageX;
client.endY = pageY;
// 清除以上一次 beginPath 之后的所有路径,进行绘制
ctx.beginPath();
// 根据配置文件设置相应配置
ctx.lineWidth = config.lineWidth;
ctx.strokeStyle = config.strokeStyle;
ctx.lineCap = config.lineCap;
ctx.lineJoin = config.lineJoin;
// 设置画线起始点位
ctx.moveTo(client.endX, client.endY);
// 监听 鼠标移动或手势移动
window.addEventListener(mobileStatus ? "touchmove" : "mousemove", draw);
};
// 绘制
const draw = (event) => {
showTip.value = false;
// 获取当前坐标点位
const { pageX, pageY } = mobileStatus ? event.changedTouches[0] : event;
// 修改最后一次绘制的坐标点
client.endX = pageX;
client.endY = pageY;
// 根据坐标点位移动添加线条
ctx.lineTo(pageX, pageY);
// 绘制
ctx.stroke();
};
// 结束绘制
const cloaseDraw = () => {
// 结束绘制
ctx.closePath();
// 移除鼠标移动或手势移动监听器
window.removeEventListener("mousemove", draw);
};
// 创建鼠标/手势按下监听器
window.addEventListener(mobileStatus ? "touchstart" : "mousedown", init);
// 创建鼠标/手势 弹起/离开 监听器
window.addEventListener(mobileStatus ? "touchend" : "mouseup", cloaseDraw);
// 取消-清空画布
const cancel = () => {
// 清空当前画布上的所有绘制内容
ctx.clearRect(0, 0, config.width, config.height);
showTip.value = true;
};
// 保存-将画布内容保存为图片
const save = () => {
// 将canvas上的内容转成blob流
var imgData = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
var lOffset = canvas.width,
rOffset = 0,
tOffset = canvas.height,
bOffset = 0;
for (var i = 0; i < canvas.width; i++) {
for (var j = 0; j < canvas.height; j++) {
var pos = (i + canvas.width * j) * 4;
if (imgData[pos] > 0 || imgData[pos + 1] > 0 || imgData[pos + 2] || imgData[pos + 3] > 0) {
// 说第j行第i列的像素不是透明的
// 楼主貌似底图是有背景色的,所以具体判断RGBA的值可以根据是否等于背景色的值来判断
bOffset = Math.max(j, bOffset); // 找到有色彩的最底部的纵坐标
rOffset = Math.max(i, rOffset); // 找到有色彩的最右端
tOffset = Math.min(j, tOffset); // 找到有色彩的最上端
lOffset = Math.min(i, lOffset); // 找到有色彩的最左端
}
}
}
if (lOffset === config.width && rOffset === 0 && tOffset === config.height && bOffset === 0) {
// eslint-disable-next-line
ElMessage({
message: "请签名后保存!",
type: "warning",
});
return;
}
const trimmedWidth = rOffset - lOffset + 1;
const trimmedHeight = bOffset - tOffset + 1;
const trimmedCanvas = document.createElement("canvas");
trimmedCanvas.width = trimmedWidth;
trimmedCanvas.height = trimmedHeight;
const trimmedContext = trimmedCanvas.getContext("2d");
trimmedContext.putImageData(
ctx.getImageData(lOffset, tOffset, trimmedWidth, trimmedHeight),
0,
0
);
const newUrl = trimmedCanvas.toDataURL();
const newImage = new Image();
newImage.src = newUrl;
console.log(trimmedWidth < trimmedHeight);
if (trimmedWidth < trimmedHeight) {
newImage.style.height = "100px";
newImage.style.transform = "rotate(-" + 90 + "deg)";
} else {
newImage.style.width = "100px";
}
// console.log(newImage.outerHTML + "</img>");
handleClose(newImage.outerHTML + "</img>");
};
// 示例文字
let showTip = ref(true);
</script>
效果:
声明
本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。