CanvasView.vue 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. <template>
  2. <div class="wrap">
  3. <div ref="signatureArea" class="signature" @mousedown="startDrawing" @mousemove="drawing" @mouseup="stopDrawing" />
  4. <div class="btn_group">
  5. <button class="btn" style="background: #1e9fff" @click="handleSave">确认完成</button>
  6. <button class="btn" style="background: #009688" @click="handleClearUp">撤回一笔</button>
  7. <button class="btn" style="background: #ff5722" @click="handleClear">重新签名</button>
  8. <button class="btn" style="background: #009688" @click="handleSave1">手动上传</button>
  9. </div>
  10. </div>
  11. </template>
  12. <script>
  13. export default {
  14. name: 'SignPage',
  15. data() {
  16. return {
  17. drawingCanvas: '',
  18. backgroundCanvas: '',
  19. drawingCtx: '',
  20. backgroundCtx: '',
  21. isDrawing: false,
  22. lastX: null,
  23. lastY: null,
  24. penstyle: { w: 5, color: 'black' }, // 画笔样式 w:画笔粗细,像素 color:颜色
  25. undoList: [], // 用于保存所有操作,用于撤销和重做
  26. // redoList: [], // 用于保存所有撤销的操作,用于重做
  27. currentPath: [] // 当次绘画操作记录
  28. }
  29. },
  30. created() {
  31. // 创建两个canvas元素
  32. this.drawingCanvas = document.createElement('canvas') // 用于绘制签名
  33. this.backgroundCanvas = document.createElement('canvas') // 用于保存带有白色背景的签名图像
  34. },
  35. mounted() {
  36. this.$nextTick(() => {
  37. this.init() // 解决初次打开页面后触发后未正常显示线条问题(dom未加载完毕,canvas未获取到宽高导致)
  38. })
  39. },
  40. methods: {
  41. init() {
  42. this.undoList = []
  43. this.drawingCtx = this.drawingCanvas.getContext('2d') // 获取绘制画布的上下文对象
  44. this.backgroundCtx = this.backgroundCanvas.getContext('2d') // 获取背景画布的上下文对象
  45. this.drawingCanvas.width = this.$refs.signatureArea.offsetWidth // 画布宽
  46. this.drawingCanvas.height = parseInt((this.$refs.signatureArea.offsetWidth / 16.0) * 9) // 画布高 16:9
  47. this.$refs.signatureArea.style.background = 'white' // 签名区域背景色
  48. this.backgroundCanvas.width = this.drawingCanvas.width
  49. this.backgroundCanvas.height = this.drawingCanvas.height
  50. this.$refs.signatureArea.appendChild(this.drawingCanvas)
  51. },
  52. // 鼠标按下
  53. startDrawing(e) {
  54. e.preventDefault() // 阻止默认事件
  55. var touch = e // 获取触摸点坐标
  56. var rect = this.$refs.signatureArea.getBoundingClientRect() // 获取签名区域的位置和大小
  57. this.lastX = touch.clientX - rect.left
  58. this.lastY = touch.clientY - rect.top
  59. this.isDrawing = true
  60. this.currentPath = { index: this.undoList.length + 1, points: [{ x: this.lastX, y: this.lastY }] } // 存储当前次的绘制信息(此时存储的points可以看做是此次笔的起始落脚点位置)
  61. },
  62. // 开始绘画
  63. drawing(e) {
  64. if (!this.isDrawing) return
  65. var touch = e
  66. var rect = this.$refs.signatureArea.getBoundingClientRect()
  67. var x = touch.clientX - rect.left
  68. var y = touch.clientY - rect.top
  69. this.currentPath.points.push({ x: x, y: y }) // 鼠标不停地滑动,不停地记录此次滑动过的位置
  70. this.drawLine(this.lastX, this.lastY, x, y) // 鼠标滑过位置,绘制显示出实线
  71. this.lastX = x // 不停滑动,此值要不停更新,旨在每一次移动都需要更新起点,即下一笔的起点是上一笔画的终点位置
  72. this.lastY = y
  73. },
  74. // 绘制实线
  75. drawLine(x1, y1, x2, y2) {
  76. this.drawingCtx.beginPath() // 开始一条新的路径
  77. this.drawingCtx.moveTo(x1, y1) // 将画笔移动到起点
  78. this.drawingCtx.lineTo(x2, y2) // 绘制一条直线到终点
  79. this.drawingCtx.lineWidth = this.penstyle.w // 设置线条的宽度为1像素
  80. this.drawingCtx.strokeStyle = this.penstyle.color // 设置线条颜色为黑色
  81. this.drawingCtx.stroke() // 绘制线条
  82. },
  83. // 停止绘画 (鼠标松开事件)
  84. stopDrawing(e) {
  85. this.isDrawing = false
  86. this.undoList.push(this.currentPath)
  87. },
  88. // 重新签名
  89. handleClear() {
  90. // 清除签名
  91. this.drawingCtx.clearRect(0, 0, this.drawingCanvas.width, this.drawingCanvas.height) // 清除绘制画布的内容
  92. this.backgroundCtx.clearRect(0, 0, this.backgroundCanvas.width, this.backgroundCanvas.height) // 清除背景画布的内容
  93. },
  94. // 撤销
  95. handleClearUp() {
  96. // 从栈中弹出最后一个状态,并重置 canvas
  97. if (this.undoList.length > 0) {
  98. const lastPath = this.undoList.pop() // 删除最后一笔画
  99. // this.redoList.push(lastPath); //记录下来撤销了哪些内容
  100. this.drawingCtx.clearRect(0, 0, this.drawingCanvas.width, this.drawingCanvas.height) // 清空画布
  101. this.drawPaths(this.undoList)// 将记录的笔画记录重新显示画出来
  102. }
  103. },
  104. // 画所有的路径
  105. drawPaths(paths) {
  106. paths.forEach(path => {
  107. this.drawingCtx.beginPath()
  108. this.drawingCtx.strokeStyle = this.penstyle.color
  109. this.drawingCtx.lineWidth = this.penstyle.w
  110. this.drawingCtx.moveTo(path.points[0].x, path.points[0].y)
  111. path.points.slice(1).forEach(point => {
  112. this.drawingCtx.lineTo(point.x, point.y)
  113. // console.error(point.x, point.y);
  114. })
  115. this.drawingCtx.stroke()
  116. })
  117. },
  118. // 保存签名
  119. handleSave() {
  120. this.$message.info('handle save')
  121. // 绘制白色背景
  122. this.backgroundCtx.fillStyle = 'white'
  123. this.backgroundCtx.fillRect(0, 0, this.backgroundCanvas.width, this.backgroundCanvas.height)
  124. // 复制绘制的签名到带有白色背景的画布
  125. this.backgroundCtx.drawImage(this.drawingCanvas, 0, 0)
  126. // 将带有白色背景的画布内容转为PNG格式的DataURL
  127. var dataURL = this.backgroundCanvas.toDataURL('image/png')
  128. // 这里的dataURL是一个base64字符串,也可以直接放到img的src里面展示
  129. const file = this.base64ImgtoFile(dataURL, '手写签名' + this.randomString(12)) // base64转file
  130. // 手写签名图片文件传递给父组件
  131. this.$emit('signatruefile', {
  132. raw: file,
  133. size: file.size,
  134. name: '手写签名' + this.randomString(12) + '.png',
  135. uid: this.randomString(12)
  136. })
  137. // 创建一个链接元素并设置下载属性
  138. // var link = document.createElement("a");
  139. // var time = new Date().getTime();
  140. // link.href = dataURL;
  141. // link.download = "签名" + time + ".png"; // 设置下载文件的名称
  142. // link.style.display = "none";
  143. // document.body.appendChild(link);
  144. // link.click();
  145. // document.body.removeChild(link);
  146. }, // 手动上传签名图片回到给父组件事件
  147. handleSave1() {
  148. this.$emit('handleSave')
  149. },
  150. // base64转file
  151. base64ImgtoFile(dataurl, filename = 'file') {
  152. const arr = dataurl.split(',')
  153. const mime = arr[0].match(/:(.*?);/)[1]
  154. const suffix = mime.split('/')[1]
  155. const bstr = atob(arr[1])
  156. let n = bstr.length
  157. const u8arr = new Uint8Array(n)
  158. while (n--) {
  159. u8arr[n] = bstr.charCodeAt(n)
  160. }
  161. return new File([u8arr], `${filename}.${suffix}`, {
  162. type: mime
  163. })
  164. },
  165. randomString(len) {
  166. len = len || 32
  167. const timestamp = new Date().getTime()
  168. /** **默认去掉了容易混淆的字符oOLl,9gq,Vv,Uu,I1****/
  169. const $chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678'
  170. const maxPos = $chars.length
  171. let randomStr = ''
  172. for (let i = 0; i < len; i++) {
  173. randomStr += $chars.charAt(Math.floor(Math.random() * maxPos))
  174. }
  175. return randomStr + timestamp
  176. }
  177. }
  178. }
  179. </script>
  180. <style scoped>
  181. .wrap {
  182. width: 640px;
  183. height: 480px;
  184. display: flex;
  185. justify-content: center;
  186. align-items: center;
  187. flex-direction: column;
  188. }
  189. .btn_group {
  190. margin-top: 16px;
  191. }
  192. .btn {
  193. width: 140px;
  194. line-height: 38px;
  195. text-align: center;
  196. font-weight: bold;
  197. letter-spacing: 2px;
  198. color: #fff;
  199. text-shadow: 1px 1px 1px #333;
  200. border-radius: 5px;
  201. margin: 0 20px 20px 20px;
  202. /* border: 1px solid #b42323; */
  203. background-color: rgb(207, 41, 41);
  204. cursor: pointer;
  205. }
  206. .signature {
  207. width: 100%;
  208. border: 1px solid #868080;
  209. }
  210. </style>