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

文章目录

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

注:本文章是给自己看的,读者酌情考虑!

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

script 代码

js 复制代码
let bigBoxHeight = ref(0);
// 获取到元素
let bigBox = document.querySelector(".bigBox");
// 设置高度为 auto
bigBox.style.height = "auto";
// 获取 offsetHeight --》 真实高度
const height = bigBox.offsetHeight;
// 设置值
bigBoxHeight.value = height;

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

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

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

js 复制代码
// 是否缩放,来确定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";
  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 使用细节

这里主要粘贴签字的代码

js 复制代码
<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, // 高度
  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) => {
  console.log(event);
  // 获取当前坐标点位
  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);
};
// 保存-将画布内容保存为图片
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 !

相关推荐
前端大卫3 分钟前
Vue3 + Element-Plus 自定义虚拟表格滚动实现方案【附源码】
前端
却尘19 分钟前
Next.js 请求最佳实践 - vercel 2026一月发布指南
前端·react.js·next.js
ccnocare20 分钟前
浅浅看一下设计模式
前端
Lee川23 分钟前
🎬 从标签到屏幕:揭秘现代网页构建与适配之道
前端·面试
Ticnix1 小时前
ECharts初始化、销毁、resize 适配组件封装(含完整封装代码)
前端·echarts
纯爱掌门人1 小时前
终焉轮回里,藏着 AI 与人类的答案
前端·人工智能·aigc
twl1 小时前
OpenClaw 深度技术解析
前端
崔庆才丨静觅1 小时前
比官方便宜一半以上!Grok API 申请及使用
前端
星光不问赶路人1 小时前
vue3使用jsx语法详解
前端·vue.js
天蓝色的鱼鱼1 小时前
shadcn/ui,给你一个真正可控的UI组件库
前端