vue+vueEsign+html2canvas合同签名上传

Thursday , 2022-7-28 15:12

一个vue+vant+vueEsign+html2canvas实现的合同签名上传demo,最终效果如下。不过似乎自己写的这样的签名合同不具有法律效用,如果生产项目中要用到且对法律比较敏感,建议还是找第三方有法律效力的机构对接。

此项目仅供学习参考,需要源码可评论留下邮箱。

20220728_142603_20220728143046.gif

思路

要实现合同的动态加载且生成带签名的图片:

  1. 首先是加载合同,这个很简单,先在本地写好合同模板,如果涉及到业务数据,则通过后端请求业务字段填入即可。
  2. 然后是签名,这里使用了vueEsign插件,来生成canvas的签名图片
  3. 将生成的图片添加到已经生成好的业务模板中,将整个页面生成图片后,上传服务器,这里使用到html2canvas插件来生成

开整

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81

//index.vue

<template>
<div id="home">

<div class="canvasBox" v-if="state.canvasImgRes">
<img :src="state.canvasImgRes" class="canvasImgRes">
</div>

<div v-else>
<Doc :signImg="state.signImg" />
<Sign @signImg="setSignImg" />
</div>

</div>
</template>

<script lang="ts" setup>
import { reactive } from "vue";
import html2canvas from 'html2canvas';

import Sign from './comp/sign.vue';
import Doc from './comp/doc.vue';

import { Toast } from 'vant';

const state = reactive({
signImg: '',//签名生成的图片
canvasImgRes: ''//合同canvas生成的结果
});

const uploadApi = async ()=>{
Toast.loading({
message:'合同上传中,请勿退出!',
duration:0,
forbidClick: true
});
try {
// state.canvasImgRes 上传base64数据,请求自定的api
// await uplaodImg({base64Imager:state.canvasImgRes});
setTimeout(() => {
Toast.success('已上传');
}, 1000);
} catch (error) {
Toast.success('上传失败');
}

}

const createDocCanvas = () => {//创建合同的整个图片
html2canvas(document.getElementById('doc') as HTMLElement).then((canvas) => {
state.canvasImgRes = canvas.toDataURL("image/png");
uploadApi();
});
}

const setSignImg = (imgBase64: string) => {//签名完成
state.signImg = imgBase64;
console.log(imgBase64);
setTimeout(() => {
createDocCanvas();
}, 500);
}

</script>


<style lang="scss" scoped>
#home {
.canvasImgRes {
display: block;
width: 100%;
}
}
.canvasBox{
text-align: center;
height: 100vh;
overflow-y: auto;
}
</style>

接下来我们封装两个核心的签名与合同组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// ./comp/doc.vue
// 这里封装了一个 DocText 组件,因为合同的内容通常很多,最好通过单独封装一个组件来显示合同模板与请求接口中的业务数据,方便此组件的维护,都是常规操作,这里不做赘述,这个视个人情况而定
// 这里需要注意的是v-html的样式需要在组件内定义样式无法生效,scss要:deep的方式,或者写咋最外层,看个人习惯

<template>
<div class="doc" id="doc">
<div class="main" id="domHtml" v-if="docInfo">
<h1>长城贴瓷砖项目协议</h1>
<DocText/>
<img :src="signImg" class="signImg" v-if="signImg">
</div>
</div>
<!-- doc -->

<div style="height:70px;"></div>
</template>

<script lang="ts" setup>
import { onMounted, ref, PropType } from "vue";
import DocText from './docText.vue';

const docInfo = ref<any>(null);//合同信息

// 当签名组件完成签名图片生成后,传入当前组件,你可以将它放在任何dom中,且自定义样式
defineProps({
signImg: {
type: String as PropType<string>,
},
});

</script>

<style lang="scss" scoped>
.signImg{
width:100%;
height: auto;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140

// './comp/sign.vue';
// 核心的签名组件,用户可以签名与重写

<template>
<div class="showBtn">
<van-button type="primary" @click="show=true">客户确认签名</van-button>
</div>

<van-popup v-model:show="show" position="bottom">

<div class="sign">
<div class="text">请客户在虚线区域内签名</div>
<div class="signBorder">
<vueEsign ref="esign" v-bind="signConfig" class="signMain" />
</div>
<!-- signBorder -->
<div class="btns">
<div class="item" @click="handleReset">清空重写</div>
<div class="item" @click="handleGenerate">确认提交</div>
</div>
<!-- btns -->
</div>

</van-popup>
</template>

<script lang="ts" setup>
import { reactive, ref } from "vue";
import vueEsign from "vue-esign";
import { Toast } from "vant";
import {useState} from '@/store/state';
const show = ref<boolean>(false);
const State = useState();
const emit = defineEmits(['signImg']);

interface Esign {
reset: any | undefined;
generate: any | undefined;
}
const signConfig = reactive({
lineWidth: 6,
lineColor: "#000000",
bgColor: "#ffffff00",//这里配置的是透明背景色
resultImg: "",
isCrop: false,
isClearBgColor: false,
height:340
});
const esign = ref<Esign>({ reset: undefined, generate: undefined });
const signImg = ref("");

const handleReset = () => {
esign.value.reset();
};

const handleGenerate = () => {
Toast.loading({
message: "合同生成中,请稍候!",
duration: 0,
forbidClick: true,
});
try {
esign.value
.generate()
.then((res: string) => {
signImg.value = res;
emit("signImg", res);//将signImg生成的base64图片回传
})
.catch((err: any) => {
Toast.fail("请签名!");
});
} catch (error) {
Toast.fail("合同生成失败,请重试!");
}
};
</script>

<style lang="scss" scoped>
.sign {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
background: white;
z-index: 111;
box-shadow: 0px -5px 20px rgba($color: #000000, $alpha: 0.15);
.signImg {
display: block;
width: 100%;
}
.signBorder {
border: 2px dotted rgba($color: #000000, $alpha: 0.2);
margin: 5px;
}
.text {
color: gray;
text-align: center;
font-size: 12px;
padding: 10px;
}
.btns {
padding: 10px;
display: flex;
.item {
margin: 10px;
flex: 1;
color: white;
border: none;
text-align: center;
font-size: 14px;
padding: 10px;
border-radius: 100px;
&:nth-child(1) {
background: #ff2b2b;
}
&:nth-child(2) {
background: #1c73ff;
}
&:active {
opacity: 0.8;
}
}
}
}
.showBtn{
position: fixed;
bottom: 0;
width: 100%;
z-index: 11;
padding: 10px;
background: white;
box-shadow: 0px 0px 10px rgba($color: #000000, $alpha: 0.2);
text-align: right;
button{
width: 100%;
position: relative;
}
}
</style>

这样最简单的一个在线合同签名上传就能实现了,当然其实最应该研究的是,vueEsign、html2canvas这两个的插件的实现原理。

就酱,完!