电子签名
应用场景
交费的电子签名,可确认和重签(清空内容),手机端默认横屏,并对内容做旋转,如图
知识点
1.vue-esign签名插件
2.手机的横屏设置
transform: translateX(-50%) translateY(-50%) rotateZ(90deg);
3.生成图片的图片的旋转
rotateBase64Img(src, edg, fileName, fileType, callback)
4.将 base64的图片 Uint8Array转换为 file 对象
/**
* 将 base64 转换为 file 对象
* dataURL:base64 格式
* fileName:文件名
* fileType:文件格式
*/
export function dataURLtoFile(dataURL, fileName, fileType) {
const dataArr = dataURL.split(',')
const byteString = atob(dataArr[1])
const options = {
type: 'image/jpeg',
endings: 'native'
}
const u8Arr = new Uint8Array(byteString.length)
for (let i = 0; i < byteString.length; i++) {
u8Arr[i] = byteString.charCodeAt(i)
}
return new File([u8Arr], fileName + '.jpg', options)
}
5.封装组件 HandSign
使用方法
安装vue-esign与main.js中引入
npm install vue-esign --save
// main.js中引入签名
import vueEsign from 'vue-esign'
Vue.use(vueEsign)
实例代码
父组件
<template>
<SignCanvas ref="SignCanvasRef" :signName="name" @sureHandler="sureSignHandler" />
</template>
<script>
import SignCanvas from '@/components/SignCanvas/index'
import {addFile} from '@/api/tools/localStorage'
export default {
name: 'HandSign',
components: {SignCanvas},
data() {
return {
id:'',
name:'',
signFile:null
}
},
created() {
this.id=this.$route.query.id
// this.name=decodeURIComponent(this.$route.query.name)
this.name=this.$route.query.name
console.log(this.name)
},
methods:{
sureSignHandler(data) {
console.log(data)
// addFile({id:this.id,name:this.name,file:data}).then(res=>{
// this.$message({
// showClose: true,
// message: '签名已经保存成功',
// type: 'success'
// });
// })
this.submitHandler(data)
},
submitHandler(data) {
//利用FormData传参
const MultipartFile = new FormData()
//file 是后端接受图片的字段
MultipartFile.append('file', data)
// 然后调用你的接口 把MultipartFile 传给后端
// {id:this.id,name:this.name,file:MultipartFile}
addFile(MultipartFile,{id:this.id}).then(res=>{
this.$message({
showClose: true,
message: '签名已经保存成功',
type: 'success'
});
})
}
}
}
</script>
<style lang='scss' scoped>
</style>
签名组件SignCanvas 路径src/components/SignCanvas/index.vue
<!-- 签名组件 -->
<template>
<div class="signContainer">
<div class="btns">
<el-button type="default" round @click="resetHandler" class="van-button reset">重签</el-button>
<el-button type="primary" round @click="sureHandler" class="van-button" style="margin-left: 0px;">确认</el-button>
</div>
<vue-esign
ref="VueEsignRef"
class="vue-esign"
:width="width"
:height="height"
:lineWidth="lineWidth"
:lineColor="lineColor"
:bgColor="bgColor"
:isCrop="isCrop"
:isClearBgColor="isClearBgColor"
:format="format"
:quality="quality"
/>
<div :style="{ '--width': height + 'px' }" class="tipText">
请<span v-if="signName">{{ ` ${signName} ` }}</span
>在此区域内签名
</div>
</div>
</template>
<script>
import { rotateBase64Img } from '@/utils/esignFun'
export default {
name: 'SignCanvas',
components: {},
props: {
// 画布宽度,即导出图片的宽度
width: {
type: Number,
default: () => {
const dom = document.querySelector('#app')
const width = dom && dom.offsetWidth
return width ? width - 60 : 300 // 减去按钮区域的宽度
}
},
// 画布高度,即导出图片的高度
height: {
type: Number,
default: () => {
const dom = document.querySelector('#app')
return (dom && dom.offsetHeight) || 800
}
},
// 画笔粗细
lineWidth: {
type: Number,
default: 6
},
// 画笔颜色
lineColor: {
type: String,
default: '#000'
},
// 画布背景色,为空时画布背景透明,支持多种格式 '#ccc','#E5A1A1','rgb(229, 161, 161)','rgba(0,0,0,.6)','red'
bgColor: {
type: String,
default: ''
},
// 是否裁剪,在画布设定尺寸基础上裁掉四周空白部分
isCrop: {
type: Boolean,
default: false
},
// 清空画布时(reset)是否同时清空设置的背景色(bgColor)
isClearBgColor: {
type: Boolean,
default: true
},
// 生成图片格式 image/jpeg(jpg格式下生成的图片透明背景会变黑色请慎用或指定背景色)、 image/webp
format: {
type: String,
default: 'image/png'
},
// 生成图片质量;在指定图片格式为 image/jpeg 或 image/webp的情况下,可以从 0 到 1 的区间内选择图片的质量。如果超出取值范围,将会使用默认值 0.92。其他参数会被忽略。
quality: {
type: Number,
default: 1
},
// 未签名时提示信息
noSignTipText: {
type: String,
default: '请确保已签名!'
},
// 需要签名的姓名
signName: {
type: String,
default: ''
}
},
methods: {
resetHandler() {
this.$refs.VueEsignRef.reset() // 清空画布
},
sureHandler() {
// 可选配置参数 ,在未设置format或quality属性时可在生成图片时配置 例如: {format:'image/jpeg', quality: 0.5}
// this.$refs.esign.generate({format:'image/jpeg', quality: 0.5})
this.$refs.VueEsignRef.generate()
.then(res => {
/**
* res:base64图片
*/
rotateBase64Img(res, 270, `${this.signName ? this.signName + '-签名.jpg' : 'sign.jpg'}`, '', data => {
this.$emit('sureHandler', data)
})
})
.catch(err => {
console.log('err----', err)
this.$alert(this.noSignTipText, '签名', {
confirmButtonText: '确定',
});
// this.$message(this.noSignTipText)
})
}
}
}
</script>
<style>
/* .el-message-box{
width: 300px !important;
}
.el-message-box__wrapper{
transform: rotate(90deg) translateY(15px);
left:auto !important;
right:auto !important;
} */
</style>
<style lang='scss' scoped>
.signContainer {
width: 100%;
height: 100vh;
display: flex;
background-color: #fff;
.btns {
width: 55px;
background-color: #f8f8f8;
display: flex;
flex-direction: column;
justify-content: center;
.reset {
margin-bottom: 70px;
}
}
.vue-esign {
z-index: 2;
}
.tipText {
position: absolute;
top: 50%;
width: var(--width);
left: calc(50% + 55px);
transform: translateX(-50%) translateY(-50%) rotateZ(90deg);
text-align: center;
color: #ddd;
letter-spacing: 2px;
}
}
.van-button {
width: 85px !important;
height: 35px;
transform: rotate(90deg) translateY(15px);
text-align: center;
.van-button__text {
letter-spacing: 5px;
}
}
::v-deep.el-message-box{
width: 300px !important;
}
::v-deep.el-message-box__wrapper{
transform: rotate(90deg) translateY(15px);
left:auto !important;
right:auto !important;
}
</style>
图片处理的js import { rotateBase64Img } from ‘@/utils/esignFun’
/**
* 图片旋转
*/
export function rotateBase64Img(src, edg, fileName, fileType, callback) {
var canvas = document.createElement('canvas')
var ctx = canvas.getContext('2d')
var imgW // 图片宽度
var imgH // 图片高度
var size // canvas初始大小
if (edg % 90 !== 0) {
console.error('旋转角度必须是90的倍数!')
return '旋转角度必须是90的倍数!'
}
edg < 0 && (edg = (edg % 360) + 360)
const quadrant = (edg / 90) % 4 // 旋转象限
const cutCoor = { sx: 0, sy: 0, ex: 0, ey: 0 } // 裁剪坐标
var image = new Image()
image.crossOrigin = 'Anonymous'
image.src = src
image.onload = () => {
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)
ctx.drawImage(image, 0, 0)
var 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
}
ctx.putImageData(imgData, 0, 0)
// callback(dataURLtoFileBlob(src))
callback(dataURLtoFile(canvas.toDataURL(), fileName, fileType))
// callback(canvas.toDataURL())
}
}
/**
* 将 base64 转换为 file 对象
* dataURL:base64 格式
* fileName:文件名
* fileType:文件格式
*/
export function dataURLtoFile(dataURL, fileName, fileType) {
const dataArr = dataURL.split(',')
const byteString = atob(dataArr[1])
const options = {
type: 'image/jpeg',
endings: 'native'
}
const u8Arr = new Uint8Array(byteString.length)
for (let i = 0; i < byteString.length; i++) {
u8Arr[i] = byteString.charCodeAt(i)
}
return new File([u8Arr], fileName + '.jpg', options)
}
// 将base64转成blob流
export function dataURLtoFileBlob (urlData) {
const type = 'image/png'
let bytes = null
if (urlData.split(',').length > 1) {
bytes = window.atob(urlData.split(',')[1])
} else {
bytes = window.atob(urlData)
}
let ab = new ArrayBuffer(bytes.length)
let ia = new Uint8Array(ab)
for (let i = 0; i < bytes.length; i++) {
ia[i] = bytes.charCodeAt(i)
}
return new Blob([ab], { type })
}