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

文章目录

最近菜鸟公司要做一个这样的功能,后端返回一个合同的整体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 !

相关推荐
轻口味1 小时前
命名空间与模块化概述
开发语言·前端·javascript
前端小小王2 小时前
React Hooks
前端·javascript·react.js
迷途小码农零零发2 小时前
react中使用ResizeObserver来观察元素的size变化
前端·javascript·react.js
娃哈哈哈哈呀2 小时前
vue中的css深度选择器v-deep 配合!important
前端·css·vue.js
旭东怪3 小时前
EasyPoi 使用$fe:模板语法生成Word动态行
java·前端·word
ekskef_sef4 小时前
32岁前端干了8年,是继续做前端开发,还是转其它工作
前端
sunshine6415 小时前
【CSS】实现tag选中对钩样式
前端·css·css3
真滴book理喻5 小时前
Vue(四)
前端·javascript·vue.js
蜜獾云5 小时前
npm淘宝镜像
前端·npm·node.js