最近项目接到一个新的需求,需要对接一个可以在线签合同的的功能,知道需要后马上开干,经过一番斗争,终于终于下班啦
开个玩笑,废话不多说,直接上代码,因为代码是直接项目中搬出来的,没有依赖其他插件纯手工,小伙伴们要看清除那些是自己需要的,不需要的就删掉啦。
目前只测试小程序 , 其他端需要自己去适配
效果图:
页面有两个,一个表单提交也需要上传身份证正反面,第二个页面有合同展示,签字面板是以弹窗形式
第一个页面(表单也不需要的可以删除):
<template>
<view>
<form @submit="formSubmit">
<view class="Rboy-box">
<view class="Rboy-obverse">
<image class="obverseimg" :src="ALMain_drawing" @click="obverse_btn" mode="aspectFill"></image>
<input style="display: none;" name="ALMain_drawing" />
<view class="bottom">
<text>身份证正面照</text>
</view>
</view>
<view class="Rboy-reverse">
<image class="reverseimg" :src="ALPicture" @click="reverse_btn" mode="aspectFill"></image>
<input style="display: none;" name="ALPicture" />
<view class="bottom">
<text>身份证反面照</text>
</view>
</view>
</view>
<view class="Rboy-form">
<view class="Rboy-form-row">
<label>姓名</label>
<view class="form-rowDom"><input placeholder="请输入姓名" disabled name="name"
:value="RealName.username" /></view>
</view>
<view class="Rboy-form-row">
<label>身份证号码</label>
<view class="form-rowDom"><input placeholder="请输入身份证" disabled name="certificates"
:value="RealName.id_card" /></view>
</view>
<view class="Rboy-form-row">
<label>电话号码</label>
<view class="form-rowDom"><input placeholder="请输入电话" disabled name="phone"
:value="RealName.phone" /></view>
</view>
<view class="Rboy-form-row">
<label>支付宝账号</label>
<view class="form-rowDom"><input placeholder="请输入电话" disabled name="phone"
:value="RealName.alipay" /></view>
</view>
</view>
<view class="agreement">
<view class="agreement-row" @click="JumpSee">
<view class="agreement-name">《自由职业合作服务协议》</view>
<view class="agreement-row-label">
<text
:style="{ color: signStatus === false?'#f97777':'#666' }">{{ signStatus === false ? '去签字':'已签署' }}</text>
<tui-icon name="arrowright" size="18"></tui-icon>
</view>
</view>
</view>
<view class="application">
<view class="application-tips" @click="readValve">
<tui-icon :color=" prevent === 'circle' ?'#999':'#fea12e' " size="14" :name="prevent"></tui-icon>
<text style="margin-left: 10upx;">我已同意并签署《自由职业合作服务协议》</text>
</view>
<button form-type="submit">提交认证</button>
<!-- <label>提交认证</label> -->
</view>
</form>
</view>
</template>
<script>
export default {
data() {
return {
identity: 0, //实名状态 ban:禁止实名 true:成功实名
ALMain_drawing: 'https://res.gaaqoo.com/mapp/img/comm/identity_topside.png', //身份默认证正面
ALPicture: 'https://res.gaaqoo.com/mapp/img/comm/identity_backside.png', //身份默认证反面
RealName: {
Main_drawing: '', //身份证正面
Picture: '', //身份证反面
username: '', //姓名
id_card: '', //身份证号
phone: '', //手机号
alipay: '', //支付宝
agreement: '' //协议
},
signStatus: false, //签署状态
prevent: 'circle',
// InformationRealName:true
}
},
async onLoad() {
const t = this;
const result = await t.$req('/user/getIdentityInfo', {})
let rd = result.data;
if (rd.code == 0) {
let data = rd.data;
t.RealName.username = data.username;
t.RealName.id_card = data.id_card;
t.RealName.phone = data.phone;
t.RealName.alipay = data.alipay;
}
},
onShow() {
const t = this;
uni.getStorage({
key: 'autograph_key',
success: function(res) {
if (res.data !== '') {
t.signStatus = true
t.RealName.agreement = res.data
}
}
});
},
methods: {
async obverse_btn() {
const t = this;
uni.chooseImage({
count: 1, //默认9
sizeType: ['original', 'compressed'], //可以指定是原图还是压缩图,默认二者都有
sourceType: ['album'], //从相册选择
success: function(res) {
let ImgType = res.tempFilePaths[0].split(".")
uni.getFileSystemManager().readFile({
filePath: res.tempFilePaths[0],
encoding: 'base64',
success: async (res) => {
t.RealName.Main_drawing = 'data:image/png;base64,' + res.data;
// t.ALMain_drawing = t.RealName.Main_drawing;
uni.showLoading({
title: '加载中'
});
// console.log(res.data)
const result = await t.$req("/user/upload/img", {
uid: t.user.uid,
type: ImgType[1],
img: res.data,
place: 'identity'
})
let rd = result.data
// console.log(rd)
if (rd.code == 0) {
let data = rd.data
t.ALMain_drawing = data.url;
uni.hideLoading();
t.RealName.Main_drawing = data.url;
}
}
})
}
});
},
async reverse_btn() {
const t = this;
uni.chooseImage({
count: 1, //默认9
sizeType: ['original', 'compressed'], //可以指定是原图还是压缩图,默认二者都有
sourceType: ['album'], //从相册选择
success: function(res) {
let ImgType = res.tempFilePaths[0].split(".")
uni.getFileSystemManager().readFile({
filePath: res.tempFilePaths[0],
encoding: 'base64',
success: async (res) => {
t.RealName.Picture = 'data:image/png;base64,' + res.data;
uni.showLoading({
title: '加载中'
});
// console.log(res.data)
const result = await t.$req("/user/upload/img", {
uid: t.user.uid,
type: ImgType[1],
img: res.data,
place: 'identity'
})
// https://ganqiao-dev.oss-cn-hangzhou.aliyuncs.com 测试环境路径
let rd = result.data
// console.log(rd)
if (rd.code == 0) {
let data = rd.data
t.ALPicture = data.url;
uni.hideLoading();
t.RealName.Picture = data.url;
}
}
})
}
});
},
readValve() {
const t = this;
if (t.prevent == "circle") {
t.prevent = "circle-fill"
} else {
t.prevent = "circle"
}
},
JumpSee() {
const t = this;
uni.navigateTo({
url: '/pages/login/seeagreement'
})
},
async formSubmit(e) {
const t = this;
var formdata = e.detail.value
if (t.RealName.Main_drawing == '') {
uni.showToast({
title: '请上传身份证正面照',
icon: "none",
duration: 2000
});
return false
}
if (t.RealName.Picture == '') {
uni.showToast({
title: '请上传身份证反面照',
icon: "none",
duration: 2000
});
return false
}
if (t.RealName.agreement == '') {
uni.showToast({
title: '请签署《自由职业合作服务协议》',
icon: "none",
duration: 2000
});
return false
}
if (t.prevent == 'circle') {
uni.showToast({
title: '请同意勾选《自由职业合作服务协议》',
icon: "none",
duration: 2000
});
return false
}
let RealName = t.RealName;
const result = await t.$req('/user/ysUpdateIdentity', {
topside: RealName.Main_drawing,
backside: RealName.Picture,
agree: 1,
user_sign: RealName.agreement
})
let rd = result.data
if (rd.code == 0) {
let data = rd.data;
uni.showToast({
title: '提交成功',
icon: 'none',
default: 200
})
}
}
}
}
</script>
<style scoped lang="less">
page{background:#fff!important}.Rboy-box{width:100%;display:flex;flex-direction:row;padding:30upx 3%;align-items:center;justify-content:space-between}.Rboy-box,.Rboy-obverse{height:auto;box-sizing:border-box}.Rboy-obverse{width:48%;margin-bottom:0upx;position:relative;box-shadow:0 3px 13px rgba(0,0,0,.05);padding:0}.Rboy-reverse{width:80%;padding:20px 3%}.obverseimg{width:100%;height:240rpx}.bottom{text-align:center;margin:auto}.bottom,.bottom text{width:100%;height:80upx;line-height:80upx}.bottom text{display:block;background:#fff;font-size:14px;color:#777}.Rboy-reverse{width:48%;height:auto;box-sizing:border-box;margin-bottom:0upx;position:relative;box-shadow:0 3px 13px rgba(0,0,0,.05);padding:0}.reverseimg{width:100%;height:240rpx}.application{width:100%;height:auto;display:flex;flex-direction:column;justify-content:center;position:fixed;bottom:20upx;left:0;right:0;padding-bottom:20upx}.application button,.application label{width:90%;height:90upx;line-height:90upx;display:block;margin:auto;text-align:center;background:linear-gradient(180deg,#efc480,#f8dca5 25%,#ffd5a2 98%);border-radius:50px;font-weight:700;font-size:15px;color:#fd972e;box-shadow:0 5px 3px #fdb964}.application button:after{border:none}.Rboy-form{width:94%;height:auto;background:#fff;margin:auto;box-shadow:0 3px 13px rgba(0,0,0,.05;);box-sizing:border-box;padding:0upx 3%}.Rboy-form-row{width:100%;display:flex;flex-direction:row;justify-content:space-between;align-items:center;border-bottom:1px solid #f5f3f3;height:100upx;line-height:100upx}.Rboy-form-row label{width:25%;font-size:14px}.form-rowDom{width:75%}.form-rowDom input{width:100%;font-size:12px}.agreement{width:94%;height:auto;background:#fff;margin:30upx auto;box-shadow:0 3px 13px rgba(0,0,0,.05);box-sizing:border-box;padding:0 3%}.agreement-row{width:100%;height:auto;display:flex;flex-direction:row;justify-content:space-between;align-items:center}.agreement-name{width:70%;height:100upx;line-height:100upx;text-align:left;font-size:14px;color:#2ebbfe}.agreement-row-label{width:30%;height:100upx;line-height:100upx;display:flex;flex-direction:row;justify-content:flex-end;align-items:center}.agreement-row-label text{font-size:12px}.application-tips{width:90%;height:auto;margin:0 auto;line-height:80upx}.application-tips text{color:#999}
</style>
第二个页面:(签名页-请把接口改成自己的 不然容易报错)
<template>
<view class="content">
<view class="authentication_top">
<image v-if="PreviewContract" :src="contractImg" mode="widthFix"></image>
<!-- 创建合成画布 -->
<view class="bgCoverBox" v-if="publish"><canvas :style="{width:height +'px',height:width +'px'}" canvas-id="mycontract" class="canvsborder2"></canvas></view>
<!-- 创建合成画布 -->
</view>
<view class="authentication_bottom" v-if="NavAutograph" style="background:#ffe9e9" @click="agreeSign">
<button style="color:#f00">我已阅读上述内容,同意签字>></button>
</view>
<view class="authentication_fun" v-if="Navpreservation" style="background:#fff; ">
<!-- <button style="color: #1789ff" @click="preservationImg">保存到本地</button>
<button style="color: #1789ff" @click="ReSign">重新签署</button> -->
<button style="color:#fd972e;" @click="SigningCompleted">签署完成,去提交>></button>
</view>
<!-- 签字弹窗 status -->
<view class="signMask" v-if="autographStatus">
<view class="sigh-btns">
<button class="btn" @tap="handleCancel">取消</button>
<button class="btn" @tap="handleReset">重写</button>
<button class="btn" @tap="handleConfirm">确认</button>
</view>
<view class="sign-box">
<canvas class="mycanvas" :style="{width:width +'px',height:height +'px'}" canvas-id="mycanvas"
@touchstart="touchstart" @touchmove="touchmove" @touchend="touchend"></canvas>
<canvas canvas-id="camCacnvs" :style="{width:height +'px',height:width +'px'}"
class="canvsborder"></canvas>
</view>
</view>
<!-- 签字弹窗 end -->
</view>
</template>
<script>
var x = 20;
var y = 20;
var tempPoint = []; //用来存放当前画纸上的轨迹点
var id = 0;
var type = '';
let that;
let canvasw;
let canvash;
export default {
data() {
return {
contractImg: '../../static/motu.jpg', //合同路径
ctx: '', //绘图图像
points: [], //路径点集合,
width: 0,
height: 0,
autographStatus: false,
publish: false,
PreviewContract:true,
NavAutograph: true,
Navpreservation:false
}
},
onLoad(option) {
that = this;
id = option.id;
type = option.type;
this.ctx = uni.createCanvasContext('mycanvas', this); //创建绘图对象
//设置画笔样式
this.ctx.lineWidth = 4;
this.ctx.lineCap = 'round';
this.ctx.lineJoin = 'round';
uni.getSystemInfo({
success: function(res) {
that.width = res.windowWidth * 0.8;
that.height = res.windowHeight * 0.85;
}
});
},
onShow() {
const t = this;
uni.getStorage({
key: 'autograph_key',
success: function (res) {
if( res.data !== '' ){
t.contractImg = res.data
t.PreviewContract = true;
t.publish = false;
t.NavAutograph = false
}
}
});
},
methods: {
agreeSign() {
this.autographStatus = true;
},
//触摸开始,获取到起点
touchstart: function(e) {
let startX = e.changedTouches[0].x;
let startY = e.changedTouches[0].y;
let startPoint = {
X: startX,
Y: startY
};
/* **************************************************
#由于uni对canvas的实现有所不同,这里需要把起点存起来
* **************************************************/
this.points.push(startPoint);
//每次触摸开始,开启新的路径
this.ctx.beginPath();
},
//触摸移动,获取到路径点
touchmove: function(e) {
let moveX = e.changedTouches[0].x;
let moveY = e.changedTouches[0].y;
let movePoint = {
X: moveX,
Y: moveY
};
this.points.push(movePoint); //存点
let len = this.points.length;
if (len >= 2) {
this.draw(); //绘制路径
}
tempPoint.push(movePoint);
},
// 触摸结束,将未绘制的点清空防止对后续路径产生干扰
touchend: function() {
this.points = [];
},
/* ***********************************************
# 绘制笔迹
# 1.为保证笔迹实时显示,必须在移动的同时绘制笔迹
# 2.为保证笔迹连续,每次从路径集合中区两个点作为起点(moveTo)和终点(lineTo)
# 3.将上一次的终点作为下一次绘制的起点(即清除第一个点)
************************************************ */
draw: function() {
let point1 = this.points[0];
let point2 = this.points[1];
this.points.shift();
this.ctx.moveTo(point1.X, point1.Y);
this.ctx.lineTo(point2.X, point2.Y);
this.ctx.stroke();
this.ctx.draw(true);
},
handleCancel() {
uni.navigateBack({
delta: 1
});
},
//清空画布
handleReset: function() {
console.log('handleReset');
that.ctx.clearRect(0, 0, that.width, that.height);
that.ctx.draw(true);
tempPoint = [];
},
//将签名笔迹上传到服务器,并将返回来的地址存到本地
handleConfirm: function() {
const t = this;
if (tempPoint.length == 0) {
uni.showToast({
title: '请先签名',
icon: 'none',
duration: 2000
});
return;
}
uni.showLoading({
title: '生成中'
});
uni.canvasToTempFilePath({
canvasId: 'mycanvas',
success: function(res) {
let tempPath = res.tempFilePath;
const ctx = uni.createCanvasContext('camCacnvs', that);
ctx.translate(0, that.width);
ctx.rotate((-90 * Math.PI) / 180);
ctx.drawImage(tempPath, 0, 0, that.width, that.height);
ctx.draw();
setTimeout(() => {
//保存签名图片到本地
uni.canvasToTempFilePath({
canvasId: 'camCacnvs',
success: function(res) {
//这是签名图片文件的本地临时地址
let path = res.tempFilePath;
console.log(path, "保存签名图片到本地")
t.autographStatus = false
// 开始合成
var _this = this;
t.publish = true;
t.PreviewContract = false;
t.NavAutograph = false;
t.Navpreservation = true;
const loy = uni.createCanvasContext('mycontract', this);
// loy.clearRect(0, 0, that.width, that.height);
// loy.translate(0, 0);
// loy.rotate((0 * Math.PI) / 180);
let imgGao = ''
uni.getSystemInfo({
success:function(res){
imgGao = res.screenWidth
}
})
loy.drawImage(t.contractImg, 0, 0, 375 , 600);
loy.drawImage(path, 260, 500, 100, 50);
loy.draw();
setTimeout(() =>{
uni.canvasToTempFilePath({
canvasId: 'mycontract',
success: function(res) {
uni.hideLoading();
t.contractImg = res.tempFilePath;
// console.log("合同图片",res.tempFilePath)
}
})
} ,200)
// 合成完毕
},
fail: err => {
// console.log('fail', err);
}
},
this
);
}, 200);
}
});
},
preservationImg(){
const t = this;
uni.downloadFile({
url: t.contractImg,
success: res => {
if (res.statusCode === 200) {
uni.saveImageToPhotosAlbum({
filePath: res.tempFilePath,
success: function() {
uni.showToast({
title: '保存成功',
icon: 'none',
duration: 2000
});
},
fail: function() {
uni.showToast({
title: '保存失败',
icon: 'none',
duration: 2000
});
}
});
} else {
uni.showToast({
title: '第三方网络错误',
icon: 'none',
duration: 2000
});
}
}
});
},
SigningCompleted(){
const t = this;
let ImgType = t.contractImg.split(".")
uni.getFileSystemManager().readFile({
filePath: t.contractImg,
encoding: 'base64',
success: async(res) =>{
t.contractImg = 'data:image/png;base64,' + res.data;
uni.showLoading({
title: '提交中'
});
// console.log(res.data)
const result = await t.$req("/user/upload/img",{
uid: t.user.uid,
type: ImgType[1],
img: res.data,
place: 'sign'
})
let rd = result.data
// console.log(rd)
if(rd.code == 0){
let data = rd.data
t.contractImg = data.url;
uni.hideLoading();
uni.setStorage({
key: 'autograph_key',
data: t.contractImg,
success: function () {
console.log("签字",'success');
}
});
uni.navigateBack({ delta: 1 }); // 返回上一页
}
}
})
},
ReSign:function(){
// console.log(this.ctx)
this.autographStatus = true
}
}
}
</script>
<style>
.authentication_top{width:100%;height:90%;position:fixed;top:0;left:0;right:0;margin:auto;overflow-y:scroll;padding-bottom:40rpx}.authentication_top image{width:100%;display:inline-block}.authentication_bottom{width:100%;height:10%;position:fixed;bottom:0;left:0;right:0;margin:auto;display:flex;flex-direction:column;justify-content:center;align-items:center;background:#ffe9e9}.authentication_bottom button{background:0 0;border:none;font-size:15px;color:red;width:100%;text-align:center}.authentication_bottom button:after{display:none}.authentication_fun{width:100%;height:10%;position:fixed;bottom:30rpx;left:0;right:0;margin:auto;display:flex;flex-direction:row;justify-content:center;align-items:center;background:#ffe9e9;padding:0 3%;width:90%;border-radius:50px;box-shadow:0 3px 13px rgba(0,0,0,.2);border:4px solid #fd972e;box-sizing:border-box}.authentication_fun button{border:none;font-size:15px;background:0 0;width:100%;text-align:center}.authentication_fun button:after{display:none}.signMask{width:100%;height:100%;background:#fff;position:fixed;top:0;bottom:0;left:0;right:0;flex-direction:row}.sign-box,.signMask{margin:auto;display:flex}.sign-box{width:80%;height:90%;flex-direction:column;text-align:center}.sigh-btns,.sign-view{height:100%}.sigh-btns{margin:auto;display:flex;flex-direction:column;justify-content:space-around}.btn{margin:auto;padding:8rpx 40rpx;font-size:14px;transform:rotate(90deg);border:1rpx solid grey}.mycanvas{margin:auto 0rpx;background-color:#ececec}.canvsborder{border:1rpx solid #333;position:fixed;top:0;left:10000rpx}.bgCoverBox{width:100%;height:auto}.canvsborder2{height:700px!important}
</style>