在项目中下载安装npm install signature_pad。
Sign.vue组件代码:
<template> <div class="sign"> <div class="sign-cont" :class="isLandscape ? 'landscape' : ''"> <div ref="canvasBox" class="canvasBox"> <canvas class="canvasId" id="canvasId" /> </div> <div class="btns"> <div class="btn-tips"> 请在上方空白处书写您的姓名~ </div> <van-button plain round type="primary" @click="$emit('back')"> 取消 </van-button> <van-button round type="primary" @click="seaveImages" :loading="isLoading"> 确定并保存 </van-button> </div> <div class="top-btns"> <div class="btn-tips"> <van-icon name="edit" size="24" color="#323232" /> <div class="btn-color red" @click="changeColor('#D9001B')"></div> <div class="btn-color blue" @click="changeColor('#0000FF')"></div> <div class="btn-color black" @click="changeColor('#323232')"></div> </div> <div class="btn-text-clean" @click="overwrite">一键清除</div> </div> </div> <!-- <van-popup v-model="showTip">内容</van-popup> --> </div> </template> <script> import { fileUpload } from "@/api/mine"; import SignaturePad from 'signature_pad'; import { Toast } from "vant"; // import { Toast } from 'vant' export default { name: "sign", props: [], components: {}, data() { return { SignaturePad: null, config: { penColor: "#000000", //笔刷颜色 minWidth: 3, //最小宽度 }, isLoading: false, showTip: false, isLandscape: false, canvas: null, } }, mounted() { this.init(); }, methods: { init() { this.isLoading = false; this.canvas = document.getElementById('canvasId'); this.signaturePad = new SignaturePad(this.canvas, this.config); this.landscapeEvent(); this.showTip = true; this.winResize(); }, changeColor(color) { if (this.signaturePad) { this.signaturePad.penColor = color; this.signaturePad.clear(); } }, winResize() { const evt = 'onorientationchange' in window ? 'orientationchange' : 'resize' window.addEventListener(evt, () => { setTimeout(() => { this.landscapeEvent(); }, 10) }) }, // 旋转操作 landscapeEvent() { this.isLandscape = document.documentElement.clientWidth > document.documentElement.clientHeight; this.$nextTick(() => { this.canvas.height = this.$refs.canvasBox.clientHeight;; this.canvas.width = this.$refs.canvasBox.clientWidth; }) console.log(this.isLandscape) }, overwrite() { this.signaturePad && this.signaturePad.clear(); }, seaveImages() { if (this.signaturePad.isEmpty()) { // Toast('签名不能为空'); Toast({ message: "签名不能为空", duration: 2000, className: this.isLandscape ? "" : "sign-toast" }) } else { this.rotateBase64Img(this.signaturePad.toDataURL(), this.isLandscape ? 0 : -90, async (dataBase64) => { let file = this.dataURLtoFile(dataBase64, new Date().getTime() + ".png"); console.log(file, "file"); let formData = new FormData(); formData.append("file", file); this.isLoading = true; fileUpload(formData) .then(res => { if (res) this.$emit("sign", res.imageUrl) this.isLoading = true; }) .catch(err => { this.isLoading = false; console.log(err); }); }) } }, //将base64转换为文件.. dataURLtoFile(dataurl, filename) { let arr = dataurl.split(","), mime = arr[0].match(/:(.*?);/)[1], // eslint-disable-next-line no-undef bstr = window.atob(arr[1]), n = bstr.length, // eslint-disable-next-line no-undef u8arr = new Uint8Array(n); while (n--) { // eslint-disable-next-line no-undef u8arr[n] = bstr.charCodeAt(n); } // eslint-disable-next-line no-undef return new File([u8arr], filename, { type: mime }); }, // 签完名的图片旋转处理 rotateBase64Img(src, edg, callback) { const canvas = document.createElement('canvas') const ctx = canvas.getContext('2d') let imgW // 图片宽度 let imgH // 图片高度 let size // canvas初始大小 if (edg % 90 !== 0) { console.error('旋转角度必须是90的倍数!') return } edg < 0 && (edg = (edg % 360) + 360) const quadrant = (edg / 90) % 4 // 旋转象限 const cutCoor = { sx: 0, sy: 0, ex: 0, ey: 0 } // 裁剪坐标 const image = new Image() image.crossOrigin = 'anonymous' image.src = src image.onload = function () { console.log('加载了') imgW = image.width imgH = image.height size = imgW > imgH ? imgW : imgH canvas.width = size * 2 canvas.height = size * 2 switch (quadrant) { case 0: cutCoor.sx = size cutCoor.sy = size cutCoor.ex = size + imgW cutCoor.ey = size + imgH break case 1: cutCoor.sx = size - imgH cutCoor.sy = size cutCoor.ex = size cutCoor.ey = size + imgW break case 2: cutCoor.sx = size - imgW cutCoor.sy = size - imgH cutCoor.ex = size cutCoor.ey = size break case 3: cutCoor.sx = size cutCoor.sy = size - imgW cutCoor.ex = size + imgH cutCoor.ey = size + imgW break } ctx?.translate(size, size) ctx?.rotate((edg * Math.PI) / 180) // drawImage向画布上绘制图片 ctx?.drawImage(image, 0, 0) // getImageData() 复制画布上指定矩形的像素数据 const imgData = ctx?.getImageData(cutCoor.sx, cutCoor.sy, cutCoor.ex, cutCoor.ey) if (quadrant % 2 === 0) { canvas.width = imgW canvas.height = imgH } else { canvas.width = imgH canvas.height = imgW } // putImageData() 将图像数据放回画布 ctx?.putImageData(imgData, 0, 0) callback(canvas.toDataURL('image/png')) } } }, }; </script> <style lang="less" scoped> * { box-sizing: border-box; } .sign { width: 100vw; height: 100vh; background-color: #fff; padding: 12px; .sign-cont { position: relative; border-radius: 4px; border: 1px solid #D7D9E7; width: calc(100vw - 24px); height: calc(100vh - 24px); .canvasBox { width: calc(100vw - 24px - 60px -40px); height: calc(100vh - 24px); margin-left: 58px; margin-right: 38px; } .top-btns { position: absolute; left: 312px; top: 0; width: calc(100vh - 6.4vw); transform: rotate(90deg) translateY(-100%); transform-origin: 0 0; display: flex; align-items: center; padding: 11px 0px 11px 11px; border-bottom: 1Px solid #D7D9E7; flex-shrink: 0; height: 40Px; flex-wrap: wrap; font-size: 14px; .btn-text-clean { font-size: 14Px; font-family: PingFangSC-Regular, PingFang SC; font-weight: 400; color: #4F80FF; line-height: 20Px; text-align: left; margin-right: 16px; text-decoration-line: underline; } .btn-tips { font-size: 14Px; font-family: PingFangSC-Regular, PingFang SC; font-weight: 400; line-height: 20Px; display: flex; align-items: center; flex: 2; } .btn-color { width: 12px; height: 12px; box-shadow: 0px 2px 4px 0px rgba(185, 185, 185, 0.5); border-radius: 50%; margin-left: 16px; } .red { background: #D9001B; } .blue { background: #0000FF; } .black { background: #323232; } } .btns { // border: 1px solid red; position: absolute; left: 0; top: 0; width: calc(100vh - 6.4vw); transform: rotate(90deg) translateY(-100%); transform-origin: 0 0; display: flex; align-items: center; padding: 11Px 0 11Px 11Px; border-top: 1Px solid #D7D9E7; flex-shrink: 0; height: 60Px; flex-wrap: wrap; .btn-tips { font-size: 14Px; font-family: PingFangSC-Regular, PingFang SC; font-weight: 400; color: #363A44; line-height: 20Px; text-align: left; margin-right: 20px; flex: 1; } .van-button { height: 36Px; width: 183Px; // flex: 1; margin-right: 12Px; font-size: 12Px; } .van-button--plain.van-button--primary { color: #4F80FF; border-color: #4F80FF; background-color: #fff; } .van-button--primary { background-color: #4F80FF; border-color: #4F80FF; } } &.landscape { position: relative; .canvasBox { width: calc(100vw - 24px); height: calc(100vh - 24px - 30px -30px); margin-left: 0; margin-right: 0; } .btns { position: absolute; left: 0; height: 30px; top: calc(100vh - 24px - 30px); width: calc(100vw - 24px); transform: rotate(0deg) translateY(0); } .top-btns { position: absolute; right: 0; height: 30px; top: calc(100vh - 24px - 30px); width: calc(100vw - 24px); transform: rotate(0deg) translateY(0); } // .btn-tips { // flex: none; // } // .van-button { // flex:1; // } } } } </style> <style lang="less"> .sign-toast { transform-origin: 50% 0; transform: rotate(90deg); } </style>
引入
<!-- 签名 -->
<sign v-if="isShowSign" class="sign-cont" @sign="signEvent" @back="backEvent" />
import Sign from "./components/Sign.vue";
components: {
Sign
},
方法:
signEvent(url) {
console.log(url)
this.isShowSign = false;
this.$nextTick(() => {
document.getElementsByClassName("sign-box")[0].scrollIntoView();
})
},
backEvent() {
this.isShowSign = false;
this.$nextTick(() => {
document.getElementsByClassName("sign-box")[0].scrollIntoView();
})
},
css:
.sign-cont {
position: fixed;
top: 0;
left: 0;
transform-origin: center center;
// .safe-area(16px);
width: 100vw;
height: 100vh;
// z-index: 9999;
}
效果: