前端实现签字效果+合同展示

要做一个这样的功能,后端返回一个合同的整体html,前端进行签字,以下是一些重要思路!

获取一个高度会变的元素的高度

script 代码

javascript 复制代码
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 代码

html 复制代码
<div class="bigBox" :style="{ height: bigBoxHeight + 'px' }">
  <div class="contractBox">
    <div v-html="printData"></div>
  </div>
  <!-- 遮罩层,返回的printData里设置了可编辑,但是这里只是展示用,且修改了也不会有影响,所以就简单的加个遮罩就行了 -->
  <div class="markBox" :style="{ height: bigBoxHeight + 'px' }"></div>
</div>

获取元素设置的 transform

感谢:原生js获取元素transform的scale和rotate

javascript 复制代码
// 获取设置了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;
}

适配手机

上面设置transform是因为返回的html文档不是那么的自适应,所以菜鸟就在手机端,让其渲染700px,但是再缩小0.5倍去展示,即可解决!

css 代码

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 代码

javascript 复制代码
// 是否缩放,来确定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 代码

html 复制代码
<div class="bigBox" :style="{ height: bigBoxHeight + 'px' }">
  <div class="contractBox" :style="{ marginLeft: isScale ? bigBoxmargin + 'px' : 'auto' }">
    <div v-html="printData"></div>
  </div>
  <div class="markBox" :style="{ height: bigBoxHeight + 'px' }"></div>
</div>

签字效果

这里签字效果,菜鸟是使用 el-dialog 实现的,el-dialog 的使用方式见:element plus 使用细节

这里主要粘贴签字的代码

html 复制代码
<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="签字"
      ref="dialogBox"
      :modelValue="dialogVisible"
      :before-close="handleClose"
      @close="closeDialog"
      :close-on-click-modal="false"
      :destroy-on-close="true"
      top="0"
      width="100%"
    >
      <canvas class="canvas"></canvas>
      <template #footer>
        <div>
          <el-button type="primary" @click="save">保存</el-button>
          <el-button @click="cancel">清除</el-button>
          <el-button @click="handleClose">关闭</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>

<style lang="scss">
.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 中代码

javascript 复制代码
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继续修改为

javascript 复制代码
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 打印出来是一个元素:

html 复制代码
<img src="data:image/png;base64,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" style="transform: rotate(-90deg);">

至此算是完成了整个签字功能!

最终完善代码,可以直接使用

其实这里还有一个问题,就是不知道横屏后的用户到底是哪边横屏,可能要旋转-90deg,也可能是正90deg,这里菜鸟想的是一个简单办法,就是给个示例文字,让用户根据示例文字进行签字!

template

html 复制代码
<template>
  <div>
    <el-dialog
      title="签字"
      ref="dialogBox"
      :modelValue="dialogVisible"
      :before-close="handleClose"
      @close="closeDialog"
      :close-on-click-modal="false"
      :destroy-on-close="true"
      top="0"
      width="100%"
    >
      <div class="canvasBox" :style="{ height: config.height + 'px' }">
        <canvas class="canvas"></canvas>
        <div class="tipTxt" v-show="showTip">
          <p>示例文字!</p>
          <p class="redTxt">请按示例文字方向,正楷清晰书写。谢谢!</p>
        </div>
      </div>
      <template #footer>
        <div>
          <el-button type="primary" @click="save">保存</el-button>
          <el-button @click="cancel">清除</el-button>
          <el-button @click="handleClose(false)">关闭</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>

<style lang="scss">
.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 的展示

javascript 复制代码
<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>

实现效果:

相关推荐
熊的猫1 小时前
JS 中的类型 & 类型判断 & 类型转换
前端·javascript·vue.js·chrome·react.js·前端框架·node.js
瑶琴AI前端1 小时前
uniapp组件实现省市区三级联动选择
java·前端·uni-app
会发光的猪。1 小时前
如何在vscode中安装git详细新手教程
前端·ide·git·vscode
别拿曾经看以后~2 小时前
【el-form】记一例好用的el-input输入框回车调接口和el-button按钮防重点击
javascript·vue.js·elementui
我要洋人死2 小时前
导航栏及下拉菜单的实现
前端·css·css3
川石课堂软件测试2 小时前
性能测试|docker容器下搭建JMeter+Grafana+Influxdb监控可视化平台
运维·javascript·深度学习·jmeter·docker·容器·grafana
科技探秘人3 小时前
Chrome与火狐哪个浏览器的隐私追踪功能更好
前端·chrome
科技探秘人3 小时前
Chrome与傲游浏览器性能与功能的深度对比
前端·chrome
JerryXZR3 小时前
前端开发中ES6的技术细节二
前端·javascript·es6
七星静香3 小时前
laravel chunkById 分块查询 使用时的问题
java·前端·laravel