|
|
@@ -0,0 +1,213 @@
|
|
|
+<template>
|
|
|
+ <div class="wrap">
|
|
|
+ <div ref="signatureArea" class="signature" @mousedown="startDrawing" @mousemove="drawing" @mouseup="stopDrawing" />
|
|
|
+ <div class="btn_group">
|
|
|
+ <button class="btn" style="background: #1e9fff" @click="handleSave">确认完成</button>
|
|
|
+ <button class="btn" style="background: #009688" @click="handleClearUp">撤回一笔</button>
|
|
|
+ <button class="btn" style="background: #ff5722" @click="handleClear">重新签名</button>
|
|
|
+ <button class="btn" style="background: #009688" @click="handleSave1">手动上传</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+export default {
|
|
|
+ name: 'SignPage',
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ drawingCanvas: '',
|
|
|
+ backgroundCanvas: '',
|
|
|
+ drawingCtx: '',
|
|
|
+ backgroundCtx: '',
|
|
|
+ isDrawing: false,
|
|
|
+ lastX: null,
|
|
|
+ lastY: null,
|
|
|
+ penstyle: { w: 5, color: 'black' }, // 画笔样式 w:画笔粗细,像素 color:颜色
|
|
|
+ undoList: [], // 用于保存所有操作,用于撤销和重做
|
|
|
+ // redoList: [], // 用于保存所有撤销的操作,用于重做
|
|
|
+ currentPath: [] // 当次绘画操作记录
|
|
|
+ }
|
|
|
+ },
|
|
|
+ created() {
|
|
|
+ // 创建两个canvas元素
|
|
|
+ this.drawingCanvas = document.createElement('canvas') // 用于绘制签名
|
|
|
+ this.backgroundCanvas = document.createElement('canvas') // 用于保存带有白色背景的签名图像
|
|
|
+ },
|
|
|
+ mounted() {
|
|
|
+ this.$nextTick(() => {
|
|
|
+ this.init() // 解决初次打开页面后触发后未正常显示线条问题(dom未加载完毕,canvas未获取到宽高导致)
|
|
|
+ })
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ init() {
|
|
|
+ this.undoList = []
|
|
|
+ this.drawingCtx = this.drawingCanvas.getContext('2d') // 获取绘制画布的上下文对象
|
|
|
+ this.backgroundCtx = this.backgroundCanvas.getContext('2d') // 获取背景画布的上下文对象
|
|
|
+ this.drawingCanvas.width = this.$refs.signatureArea.offsetWidth // 画布宽
|
|
|
+ this.drawingCanvas.height = parseInt((this.$refs.signatureArea.offsetWidth / 16.0) * 9) // 画布高 16:9
|
|
|
+ this.$refs.signatureArea.style.background = 'white' // 签名区域背景色
|
|
|
+ this.backgroundCanvas.width = this.drawingCanvas.width
|
|
|
+ this.backgroundCanvas.height = this.drawingCanvas.height
|
|
|
+ this.$refs.signatureArea.appendChild(this.drawingCanvas)
|
|
|
+ },
|
|
|
+ // 鼠标按下
|
|
|
+ startDrawing(e) {
|
|
|
+ e.preventDefault() // 阻止默认事件
|
|
|
+ var touch = e // 获取触摸点坐标
|
|
|
+ var rect = this.$refs.signatureArea.getBoundingClientRect() // 获取签名区域的位置和大小
|
|
|
+ this.lastX = touch.clientX - rect.left
|
|
|
+ this.lastY = touch.clientY - rect.top
|
|
|
+ this.isDrawing = true
|
|
|
+ this.currentPath = { index: this.undoList.length + 1, points: [{ x: this.lastX, y: this.lastY }] } // 存储当前次的绘制信息(此时存储的points可以看做是此次笔的起始落脚点位置)
|
|
|
+ },
|
|
|
+ // 开始绘画
|
|
|
+ drawing(e) {
|
|
|
+ if (!this.isDrawing) return
|
|
|
+ var touch = e
|
|
|
+ var rect = this.$refs.signatureArea.getBoundingClientRect()
|
|
|
+ var x = touch.clientX - rect.left
|
|
|
+ var y = touch.clientY - rect.top
|
|
|
+ this.currentPath.points.push({ x: x, y: y }) // 鼠标不停地滑动,不停地记录此次滑动过的位置
|
|
|
+ this.drawLine(this.lastX, this.lastY, x, y) // 鼠标滑过位置,绘制显示出实线
|
|
|
+ this.lastX = x // 不停滑动,此值要不停更新,旨在每一次移动都需要更新起点,即下一笔的起点是上一笔画的终点位置
|
|
|
+ this.lastY = y
|
|
|
+ },
|
|
|
+ // 绘制实线
|
|
|
+ drawLine(x1, y1, x2, y2) {
|
|
|
+ this.drawingCtx.beginPath() // 开始一条新的路径
|
|
|
+ this.drawingCtx.moveTo(x1, y1) // 将画笔移动到起点
|
|
|
+ this.drawingCtx.lineTo(x2, y2) // 绘制一条直线到终点
|
|
|
+ this.drawingCtx.lineWidth = this.penstyle.w // 设置线条的宽度为1像素
|
|
|
+ this.drawingCtx.strokeStyle = this.penstyle.color // 设置线条颜色为黑色
|
|
|
+ this.drawingCtx.stroke() // 绘制线条
|
|
|
+ },
|
|
|
+ // 停止绘画 (鼠标松开事件)
|
|
|
+ stopDrawing(e) {
|
|
|
+ this.isDrawing = false
|
|
|
+ this.undoList.push(this.currentPath)
|
|
|
+ },
|
|
|
+ // 重新签名
|
|
|
+ handleClear() {
|
|
|
+ // 清除签名
|
|
|
+ this.drawingCtx.clearRect(0, 0, this.drawingCanvas.width, this.drawingCanvas.height) // 清除绘制画布的内容
|
|
|
+ this.backgroundCtx.clearRect(0, 0, this.backgroundCanvas.width, this.backgroundCanvas.height) // 清除背景画布的内容
|
|
|
+ },
|
|
|
+ // 撤销
|
|
|
+ handleClearUp() {
|
|
|
+ // 从栈中弹出最后一个状态,并重置 canvas
|
|
|
+ if (this.undoList.length > 0) {
|
|
|
+ const lastPath = this.undoList.pop() // 删除最后一笔画
|
|
|
+ // this.redoList.push(lastPath); //记录下来撤销了哪些内容
|
|
|
+ this.drawingCtx.clearRect(0, 0, this.drawingCanvas.width, this.drawingCanvas.height) // 清空画布
|
|
|
+ this.drawPaths(this.undoList)// 将记录的笔画记录重新显示画出来
|
|
|
+ }
|
|
|
+ },
|
|
|
+ // 画所有的路径
|
|
|
+ drawPaths(paths) {
|
|
|
+ paths.forEach(path => {
|
|
|
+ this.drawingCtx.beginPath()
|
|
|
+ this.drawingCtx.strokeStyle = this.penstyle.color
|
|
|
+ this.drawingCtx.lineWidth = this.penstyle.w
|
|
|
+ this.drawingCtx.moveTo(path.points[0].x, path.points[0].y)
|
|
|
+ path.points.slice(1).forEach(point => {
|
|
|
+ this.drawingCtx.lineTo(point.x, point.y)
|
|
|
+ // console.error(point.x, point.y);
|
|
|
+ })
|
|
|
+ this.drawingCtx.stroke()
|
|
|
+ })
|
|
|
+ },
|
|
|
+ // 保存签名
|
|
|
+ handleSave() {
|
|
|
+ this.$message.info('handle save')
|
|
|
+ // 绘制白色背景
|
|
|
+ this.backgroundCtx.fillStyle = 'white'
|
|
|
+ this.backgroundCtx.fillRect(0, 0, this.backgroundCanvas.width, this.backgroundCanvas.height)
|
|
|
+
|
|
|
+ // 复制绘制的签名到带有白色背景的画布
|
|
|
+ this.backgroundCtx.drawImage(this.drawingCanvas, 0, 0)
|
|
|
+ // 将带有白色背景的画布内容转为PNG格式的DataURL
|
|
|
+ var dataURL = this.backgroundCanvas.toDataURL('image/png')
|
|
|
+ // 这里的dataURL是一个base64字符串,也可以直接放到img的src里面展示
|
|
|
+ const file = this.base64ImgtoFile(dataURL, '手写签名' + this.randomString(12)) // base64转file
|
|
|
+ // 手写签名图片文件传递给父组件
|
|
|
+ this.$emit('signatruefile', {
|
|
|
+ raw: file,
|
|
|
+ size: file.size,
|
|
|
+ name: '手写签名' + this.randomString(12) + '.png',
|
|
|
+ uid: this.randomString(12)
|
|
|
+ })
|
|
|
+ // 创建一个链接元素并设置下载属性
|
|
|
+ // var link = document.createElement("a");
|
|
|
+ // var time = new Date().getTime();
|
|
|
+ // link.href = dataURL;
|
|
|
+ // link.download = "签名" + time + ".png"; // 设置下载文件的名称
|
|
|
+ // link.style.display = "none";
|
|
|
+ // document.body.appendChild(link);
|
|
|
+ // link.click();
|
|
|
+ // document.body.removeChild(link);
|
|
|
+ }, // 手动上传签名图片回到给父组件事件
|
|
|
+ handleSave1() {
|
|
|
+ this.$emit('handleSave')
|
|
|
+ },
|
|
|
+ // base64转file
|
|
|
+ base64ImgtoFile(dataurl, filename = 'file') {
|
|
|
+ const arr = dataurl.split(',')
|
|
|
+ const mime = arr[0].match(/:(.*?);/)[1]
|
|
|
+ const suffix = mime.split('/')[1]
|
|
|
+ const bstr = atob(arr[1])
|
|
|
+ let n = bstr.length
|
|
|
+ const u8arr = new Uint8Array(n)
|
|
|
+ while (n--) {
|
|
|
+ u8arr[n] = bstr.charCodeAt(n)
|
|
|
+ }
|
|
|
+ return new File([u8arr], `${filename}.${suffix}`, {
|
|
|
+ type: mime
|
|
|
+ })
|
|
|
+ },
|
|
|
+ randomString(len) {
|
|
|
+ len = len || 32
|
|
|
+ const timestamp = new Date().getTime()
|
|
|
+ /** **默认去掉了容易混淆的字符oOLl,9gq,Vv,Uu,I1****/
|
|
|
+ const $chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678'
|
|
|
+ const maxPos = $chars.length
|
|
|
+ let randomStr = ''
|
|
|
+ for (let i = 0; i < len; i++) {
|
|
|
+ randomStr += $chars.charAt(Math.floor(Math.random() * maxPos))
|
|
|
+ }
|
|
|
+ return randomStr + timestamp
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.wrap {
|
|
|
+ width: 640px;
|
|
|
+ height: 480px;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ flex-direction: column;
|
|
|
+}
|
|
|
+.btn_group {
|
|
|
+ margin-top: 16px;
|
|
|
+}
|
|
|
+.btn {
|
|
|
+ width: 140px;
|
|
|
+ line-height: 38px;
|
|
|
+ text-align: center;
|
|
|
+ font-weight: bold;
|
|
|
+ letter-spacing: 2px;
|
|
|
+ color: #fff;
|
|
|
+ text-shadow: 1px 1px 1px #333;
|
|
|
+ border-radius: 5px;
|
|
|
+ margin: 0 20px 20px 20px;
|
|
|
+ /* border: 1px solid #b42323; */
|
|
|
+ background-color: rgb(207, 41, 41);
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+.signature {
|
|
|
+ width: 100%;
|
|
|
+ border: 1px solid #868080;
|
|
|
+}
|
|
|
+</style>
|