简介

项目结构:html+vue+springboot
html引入VUE项目vue.min.js、jQuery的jquery-3.4.1.min.js,签名:modernizr.js、jSignature.min.js、jSignature.min.noconflict.js,手机端mui.min.js,弹窗样式:mustache.js、zeroModal.js

所用的js在这里

html5 逐字签名 h5手写签名插件_服务器

HTML

<!DOCTYPE html>
<html>
  <head>
	<meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
    <meta name="renderer" content="webkit">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
	<meta name="apple-mobile-web-app-capable" content="yes">
	<link rel="stylesheet" href="css/mui.min.css"/>
	<link rel="stylesheet" href="css/zeroModal.css"/>
    <title>签到</title>
	  <style>
	  body {
	      font-family: 'Helvetica Neue', Helvetica, sans-serif;
	      font-size: 17px;
	      line-height: 21px;
	      color: #000;
	  }
      html,
      body,
      #app {
        height: 100%;
        margin: 0px;
        padding: 0px;
        /* background-color: #F5F5F5 !important; */
      }
	  .content{
		  margin: 0.9375rem;
	  }
      .title{
      	height: 2.5rem;
      	background-color: #fef6e1;
      	color: #dc8e35;
      	padding: 0.9375rem;
      	font-size: 0.9375rem;
      	display: flex;
      	align-items: center;
      	border-radius: 0.5rem;
		margin-bottom: 0.9375rem;
      }
      .title img{
      	width: 0.9375rem;
      	height:0.9375rem; 
      	margin-right: 0.9375rem;
      }
	  .flex{
	      display: flex;
		  color: #333333;
		  border-bottom: 0.0625rem solid #efefef;
	  }
	  .left_label{
	    height: 1.25rem;
	  	display: inline-block;
	  	line-height: 1.25rem;
	  	color: #999;
		padding-top: 0.625rem;
	  }
	  .center_label{
	      flex: 1;
	      line-height: 1.25rem;
	      text-align: left;
	  }
	  .center_input{
	      flex: 1;
	      line-height: 1.25rem;
	      text-align: center;
	  }
	  .flex input{
		  border: 0;
		  margin-bottom: 0.5rem;
	  }
	  .table-view{
		  border-radius: 0.5rem;
		  border-top: 0;
		  border-bottom: 0;
		  margin-top: 0;
		  margin-bottom: 1rem;
		  padding-left: 0;
		  list-style: none;
		  background-color: #fff;
		  
	  }
	  .table-view-cell{
		  padding: 0.6875rem 0.9375rem 0 0.9375rem;
		  overflow: hidden;
	  }
	  .qm{
		  padding: 0.6875rem 0.9375rem;
	  }
	  .table-view::before{
		  position: absolute;
		  right: 0;
		  left: 0;
		  height: 1px;
		  content: '';
		  -webkit-transform: scaleY(.5);
		  transform: scaleY(.5);
		  background-color: #c8c7cc;
		  top: -1px;
	  }
	  .table-view:after {
	      position: absolute;
	      right: 0;
	      bottom: 0;
	      left: 0;
	      height: 1px;
	      content: '';
	      -webkit-transform: scaleY(.5);
	      transform: scaleY(.5);
	      background-color: #c8c7cc;
	  }
	  .memo{
		  line-height: 1.25rem;
		  text-indent: 1.875rem;
	  }
	  #signature{
		  border: 1px solid #CCCCCC;
	  }
	  .qmcenter{
		  margin-top: 1rem;
	  }
	  #image{
		  border: 1px solid #CCCCCC;
	  }
  </style>
  </head>
  <body>
    <div id="app">
		<div class="content">
			<div class="title">
				<img src="image/notice.png" />
				<span>签到</span>
			</div>
			<form>
				<ul class="table-view">
					<li class="table-view-cell mui-collapse-content flex">
						<div class="left_label" ><font style="color: red;">*</font>手机号:</div>
					    <div class="center_input">
					    	<input type="text" placeholder="请输入手机号" v-model="form.userMobile" oninvalid="userMobile"  oninput="value=value.replace(/[^\d]/g,'')">
					    </div>
					</li>
					<li class="table-view-cell mui-collapse-content flex">
						<div class="left_label" ><font style="color: red;">*</font>验证码:</div>
					    <div class="center_input">
					    	<input type="text" placeholder="请输入验证码" v-model="form.verificationCode" 
							style="width: 55%;" id="verificationCode">
							<span v-show="isViewCode" style="color: #1c6fcd;" @click="sendOutVerificationCode" >获取验证码</span>
							<span v-show="!isViewCode" style="color: #1c6fcd;">剩余时间{{count}}s</span>
					    </div>
					</li>
				</ul>
				<ul class="table-view">
					<li class="table-view-cell mui-collapse-content qm">
						<div class="left_label" >
							<font style="color: red;">*</font>签名:
							
						</div>
						<button type="button" class="mui-btn mui-btn-success"
						style="border-radius: 5px;float: right;" @click="signatureCreate">预览签名</button>
						<button type="button" class="mui-btn mui-btn-success"
						style="border-radius: 5px;float: right;margin-right: 7px;" @click="signatureReset">重置签名</button>
						<div class="center_input qmcenter">
							<div id="signatureparent">
								<div id="signature"></div>
							</div>
						</div>
					</li>
				</ul>
				<button type="button" class="mui-btn mui-btn-primary mui-btn-block" style="border-radius: 8px;" @click="submitData">提交签到</button>
			</form>
			<div id="image" style="margin:20px;display: none;"></div>
		</div>
	</div>
    <script type="text/javascript" src="js/jquery-3.4.1.min.js"></script>
	<script type="text/javascript" src="js/modernizr.js"></script>
	<script type="text/javascript" src="js/jSignature.min.js"></script>
	<script type="text/javascript" src="js/jSignature.min.noconflict.js"></script>
    <script type="text/javascript" src="js/mui.min.js"></script>
    <script type="text/javascript" src="js/vue.min.js" charset="utf-8"></script>
    <script type="text/javascript" src="js/app.js" charset="utf-8"></script>
	<script type="text/javascript" src="js/mustache.js"></script>
	<script type="text/javascript" src="js/zeroModal.js"></script>
    <script type="text/javascript">
	$(function(){
	   //初始化签名插件
	   var param= {
	   	width: '100%',//签名区域的宽
	   	height: '300px',//签名区域的高
		signatureLine: false,//去除默认画布上那条横线
	   	lineWidth: '2' //画笔的大小
	   };
	   $("#signature").jSignature(param);
	})
	
	var app = new Vue({
		el: '#app',
		data:{
			form:{
				userMobile:undefined,
				verificationCode:undefined
			},
			verificationCode:undefined,
			isViewCode: true,
			count: '',
			timer: null,
			ggxxrecid:undefined,
			recid:undefined,
			detail:{},
		},
		mounted: function() { 
			
		},
		created() {
		//业务代码初始化
			var queryStr=this.GetStrQuery();
			 if(queryStr!=null){
			 	var whereStr=queryStr.wherestr;
				if(whereStr!=null && whereStr!="" && whereStr!=undefined){
					var whereJson = JSON.parse(whereStr);
					console.log(whereJson.recid);
					this.ggxxrecid = whereJson.recid;
					setTimeout(function() {
						app.getPxggDetail(whereJson.recid);
					}, 200);
				}
			}
		},
		methods: {
		//html弹窗
			modaleAlert(content,contentDetail){
				zeroModal.alert({
					content: content,
					contentDetail: contentDetail,
					width: '60%',
					height: '40%',
					okFn: function() {
					}
				});
			},
			//是否获取允许验证码
			sendOutVerificationCode(){
				if(this.form.userMobile != null){
					mui.ajax(serverUrl + "/getPxggTrainDetail", {
						data: {
							tel:this.form.userMobile,
							recid:this.ggxxrecid
						},
						dataType: "json",
						type: 'POST', //HTTP请求类型
						timeout: 20000, //超时时间设置为10秒;
						success: function(data) {
							console.log(data);
							if (data.code==200) {
								app.recid = data.msg;
								app.getCode();
							} else {
								mui.toast("您的报名信息不存在,请确认是否用此手机号报名!");
							}
						},
						error: function(xhr, type, errorThrown) {
							mui.toast('手机号检测异常');
						}
					});
				}else {
					mui.toast('请先输入手机号!');
				}
			},
			//获取验证码
			getCode:function(){
				const userMobile = this.form.userMobile;
				const TIME_COUNT = 120;
				if (!this.timer) {
					this.count = TIME_COUNT;
					this.isViewCode = false;
					this.timer = setInterval(() => {
						if (this.count > 0 && this.count <= TIME_COUNT) {
							this.count--;
						} else {
							this.isViewCode = true;
							clearInterval(this.timer);
							this.timer = null;
						}
					}, 1000)
				}
				mui.ajax(serverUrl + "/getMobileCode", {
					data: {
						tel:this.form.userMobile
					},
					dataType: "json",
					type: 'POST', //HTTP请求类型
					timeout: 20000, //超时时间设置为10秒;
					success: function(data) {
						if (data.code==200) {
							app.verificationCode = data.data;
						} else {
							mui.toast(data.msg);
						}
					},
					error: function(xhr, type, errorThrown) {
						mui.toast('获取验证码失败');
					}
				});
			},
			//初始化获取浏览器url传参
			GetStrQuery:function () {
				var params = location.search.substr(1);//这一条语句获取了包括问号开始到参数的最后,不包括前面的路径,去掉问号
				var pa = params.split("&");
				var queryStr = new Object();
			    for(var i = 0; i < pa.length; i ++){
			        queryStr[pa[i].split("=")[0]] = unescape(pa[i].split("=")[1]);
			    }
				if(queryStr != null) return queryStr;
				return null;
			},
			//提交
			saveData:function() {
				app.imageSave();
				app.submitData();
			},
			//提交form表单
			submitData:function(){
				if (this.form.userMobile == null || this.form.userMobile == undefined || this.form.userMobile == '') {
					mui.toast("请填写手机号!");
					return;
				}
				if (this.verificationCode!=this.form.verificationCode) {
					mui.toast("验证码输入错误!");
					return;
				}
				if( $("#signature").jSignature('getData', 'native').length == 0){
					mui.toast("请先进行签名"); 
					return; 
				}
				
				this.form.recid = this.detail.recid;
				mui.ajax(serverUrl + "/editPxggTrain", {
					data: {
						data:JSON.stringify(this.form)
					},
					dataType: "json",
					type: 'POST', //HTTP请求类型
					timeout: 20000, //超时时间设置为10秒;
					success: function(data) {
						// console.log(data);
						if (data.code==200) {
							if (data.data) {
								mui.toast("保存成功!");
								app.reset();
								app.signatureReset();
							}
						} else {
							mui.toast(data.msg);
						}
					},
					error: function(xhr, type, errorThrown) {
						mui.toast('保存失败');
					}
				});
			},
			//重置表单数据
			reset:function(){
				app.form.userMobile=undefined;
				app.form.verificationCode=undefined;
				app.verificationCode=undefined;
				app.isViewCode= true;
				app.count= '';
				app.timer= null;
			},
			//重置签名画板
			signatureReset:function(){
				$("#signature").jSignature('reset');
				$("#image").attr('src','');
				document.getElementById("image").style.display="none";
			},
			//生成预览签名
			signatureCreate:function(){
				if( $("#signature").jSignature('getData', 'native').length == 0){
					mui.toast("请先进行签名"); 
					return; 
				}
				document.getElementById("image").style.display="";
				var datapair =  $("#signature").jSignature("getData", "image")
				console.log(datapair);
				var img = new Image();
				img.src = "data:" + datapair[0] + "," + datapair[1]
				$(img).appendTo($("#image"))
			},
			//上传签名图片
			imageSave:function(){
				var datapair = $("#signature").jSignature("getData","image");  //将canvas里面的数据转换成base64数组
				var imgBase64='data:' + datapair[0] + "," + datapair[1];  //封装成正确的base64
				var file= this.base64toFile(imgBase64,'file');     //base64转换成流文件,可以打印出来看下
				console.log("file====>",file);
				let formData = new FormData();            //封装成formData格式
				formData.append('file', file);
				formData.append('businessid', app.recid);
				mui.ajax(serverUrl + "/upLoadItemImage", {
					contentType: false,   //不可少
					processData: false,   //不可少
					dataType: "json",
					type: 'POST', //HTTP请求类型
					timeout: 20000, //超时时间设置为10秒;
					data : formData,
					async : false,
					success: function(data) {
						if (data.code==200) {
							if (data.data) {
								mui.toast("签名图片保存成功!");
								// app.reset();
							}
						} else {
							mui.toast(data.msg);
						}
					},
					error: function(xhr, type, errorThrown) {
						mui.toast('保存失败');
					}
				});
			},
			//重点来了,该方法将Base64格式转换成流格式
			base64toFile:function(dataurl, filename) {
				let arr = dataurl.split(',')
				let mime = arr[0].match(/:(.*?);/)[1]
				let suffix = mime.split('/')[1]
				let bstr = atob(arr[1])
				let n = bstr.length
				let u8arr = new Uint8Array(n)
				while (n--) {
					u8arr[n] = bstr.charCodeAt(n)
				}
				return new File([u8arr], `${filename}.${suffix}`, {
					type: mime
				})
			},
			//业务代码获取详情
			getPxggDetail(recid){
				mui.ajax(serverUrl + "/getPxggDetail", {
					data: {
						recid:recid
					},
					dataType: "json",
					type: 'POST', //HTTP请求类型
					timeout: 20000, //超时时间设置为10秒;
					success: function(data) {
						console.log(data);
						if (data.code==200) {
							app.detail = data.data;
							this.detail = data.data;
							var newtime = this.parseTime(new Date(),'{y}-{m}-{d} {h}:{i}:{s}');
							console.log(this.detail);
							console.log(this.detail.signInStartTime);
							console.log(this.detail.signInEndTime);
							//当前时间大于报名开始时间
							if (this.compareDate(newtime,this.detail.signInStartTime)) {
								//在时间段内
							} else {
								//报名尚未开始
								app.modaleAlert('签到尚未开始','');
								$("input[type=text]").prop('disabled','disabled');
								$("span").prop('disabled','disabled');
								$(".mui-btn").prop('disabled','disabled');
							}
							//当前时间大于报名结束时间
							if (this.compareDate(newtime,this.detail.signInEndTime)) {
								//报名已结束
								app.modaleAlert('签到已结束','');
								$("input[type=text]").prop('disabled','disabled');
								$("span").prop('disabled','disabled');
								$(".mui-btn").prop('disabled','disabled');
							} else {
								//在时间段内
							}
						} else {
							mui.toast(data.msg);
						}
					}
				});
			},
		},
	});
  </script>
  </body>
  
</html>

java服务端

@ApiOperation("手机端保存签名图片")
    @RequestMapping(value = "/upLoadItemImage", method = { RequestMethod.POST }, produces = "application/json;charset=UTF-8")
    public @ResponseBody AjaxResult upLoadItemImage(HttpServletRequest request, HttpServletResponse response){
        try {
        //这个是图片保存表的外键id
            String businessid=request.getParameter("businessid");
            if (StringUtils.isEmpty(businessid))
            {
                return AjaxResult.error("缺少必要参数");
            }
            String userName = "";
            Date now = Date.from(Instant.now());
            boolean istrue=false;
            //附件表
            SysAttachment attachment = new SysAttachment();
            // 判断 request 是否有文件上传,即多部分请求
            if (multipartResolver.isMultipart(request)) {
                // 转换成多部分request
                MultipartHttpServletRequest multiRequest = (MultipartHttpServletRequest) multipartResolver.resolveMultipart(request);
                // 取得request中的所有文件名
                Iterator<String> iter = multiRequest.getFileNames();
                while (iter.hasNext()) {
                    // 记录上传过程起始时的时间,用来计算上传时间
                    int pre = (int) System.currentTimeMillis();
                    // 取得上传文件
                    MultipartFile file = multiRequest.getFile(iter.next());
                    if (file != null) {
                        // 取得当前上传文件的文件名称
                        String fileName = file.getOriginalFilename();
                        // 如果名称不为“”,说明该文件存在,否则说明该文件不存在
                        if (fileName.trim() != "") {
                            //获取原始文件名、后缀和文件大小
                            long size = file.getSize()/ 1024;
                            String extension = FileUploadUtils.getExtension(file);
                            // 上传并返回新文件路径名称 YaWeiConfig.getUploadPath()是上传的路径比如D:/ruoyi/xxx
                            String pathFileName = FileUploadUtils.upload(YaWeiConfig.getUploadPath(), file);
                            attachment.setFileName(fileName);
                            //附件类型
                            attachment.setModule(WhythConstants.TRAIN.HANDWRITE_SIGN_IN_MUDOLE);
                            attachment.setBusinessid(businessid);
                            attachment.setPath(pathFileName);
                            attachment.setAttachmentSize(size);
                            attachment.setSuffix(extension);
                            attachment.setCreateBy(userName);
                            attachment.setCreateTime(now);
                            attachment.setUpdateBy(userName);
                            attachment.setUpdateTime(now);
                            istrue=iSysAttachmentService.save(attachment);
                        }
                    }
                }
            }
            if(istrue){
                return AjaxResult.success(attachment);
            }else{
                return AjaxResult.error("上传失败!");
            }
        } catch (Exception e) {
            e.printStackTrace();
            logger.error("上传失败:", e);
            return AjaxResult.error("签名图片保存失败!失败信息:"+e.getMessage());
        }
    }