先上效果图,不满意可以直接关闭这页签


新建成单独的组件,然后具体功能引入,具体功能点击签名按钮,把当前功能页面用样式隐藏掉,v-show和v-if也行,然后再把这个组件显示出来。
【签名-撤销】原理是之前绘画时把全部轨迹路径都记录下来,然后点击撤销时,清空画布,路径数组去掉最后一次绘画动作,然后再把剩余路径又全部画上去,这么干后会路径会出现锯齿,我也莫得办法了,将就着用了。
【签名-完成】点击完成会判断路径数组是否有值,如果没有则说明没有签名,有则把画布保存成图片,然后再把这个图片以指定尺寸画入另一个画布,避免保存下来的图片分辨率过大,导致文件太大。画上另一个画布之后,这个画布再保存成图片,然后图片再转成base64格式返回给主页面,要是不想转base64可以把具体代码去掉。生成的图片是垂直,应该以逆时针旋转90度保存的,奈何前端实力不过关,怎么都处理不好这个逆时针旋转90度,只能在上传到后端后,用后端旋转了(丢人).........如果有人能处理好这个,麻烦评论留下代码
主页面引入组件并注册,然后用v-show控制是否显示,主页面样式自己调整好,让电子签名可以覆盖整个页面。

[具体组件代码]
- <view class="panel" :style="{top: `${top}px`}">
- <canvas canvas-id="signCanvas" class="sign-canvas" @touchstart="handleTouchStart"
- @touchmove="handleTouchMove" @touchend="handleTouchEnd">canvas>
-
- <canvas canvas-id="signCanvasReduce" class="sign-canvas-reduce">canvas>
- <view class="panel-bottom">
- <view class="panel-bottom-btn btn-gray" @click="onCancel">取消view>
- <view class="panel-bottom-btn btn-gray" @click="onUndo">撤销view>
- <view class="panel-bottom-btn btn-gray" @click="onClearRect(true)">重写view>
- <view class="panel-bottom-btn" @click="onSaveSign">完成view>
- view>
- view>
-
- <script>
- export default {
- data() {
- return {
- // 距离顶部高度
- top: void (0),
- isDrawing: false,
- startX: 0,
- startY: 0,
- strokes: [],
- canvasWidth: 0,
- canvasHeight: 0
- }
- },
-
- mounted() {
- // 获取手机状态栏和导航栏高度,和手机屏幕可用宽高度
- const _this = this
- uni.getSystemInfo({
- success: function(res) {
- _this.canvasWidth = res.windowWidth
- _this.canvasHeight = res.windowHeight
- const custom = uni.getMenuButtonBoundingClientRect()
- // 导航栏胶囊高度 + (胶囊距离顶部高度 - 状态栏高度) * 2 + 状态栏高度 + 20内边距
- _this.top = custom.height + (custom.top - res.statusBarHeight) * 2 + res.statusBarHeight + 4
- }
- })
- // 创建画布
- const ctx = uni.createCanvasContext('signCanvas', this)
- ctx.setStrokeStyle('#000')
- ctx.setLineWidth(4)
- ctx.setLineCap('round')
- ctx.setLineJoin('round')
- ctx.draw()
- this.canvasContext = ctx
- },
-
- methods: {
-
- handleTouchStart(e) {
- // 阻止默认滚动行为
- e.preventDefault()
- const touch = e.touches[0]
- this.isDrawing = true
- this.startX = touch.x
- this.startY = touch.y
- this.strokes.push({
- type: 'start',
- x: touch.x,
- y: touch.y
- })
- },
-
- handleTouchMove(e) {
- e.preventDefault() // 阻止默认滚动行为
- if (!this.isDrawing) {
- return
- }
- const touch = e.touches[0]
- this.canvasContext.moveTo(this.startX, this.startY)
- this.canvasContext.lineTo(touch.x, touch.y)
- this.canvasContext.stroke()
- this.canvasContext.draw(true)
- this.startX = touch.x
- this.startY = touch.y
- this.strokes.push({
- type: 'move',
- x: touch.x,
- y: touch.y
- })
- },
-
- handleTouchEnd(e) {
- e.preventDefault() // 阻止默认滚动行为
- this.isDrawing = false
- },
-
- // 撤销
- onUndo () {
- // 先清空当前画布
- this.onClearRect(false)
- if (this.strokes.length) {
- // 去掉最后一次绘画的路径
- while (this.strokes.pop().type !== 'start'){}
- // 剩余路径全部绘制到画布上
- for (let i = 0; i < this.strokes.length; i++) {
- const item = this.strokes[i]
- if(item.type === 'start') {
- // 绘制起始点
- this.canvasContext.beginPath()
- this.canvasContext.moveTo(item.x, item.y)
- } else if(item.type === 'move') {
- // 绘制线条
- this.canvasContext.lineTo(item.x, item.y)
- this.canvasContext.stroke()
- }
- }
- this.canvasContext.draw(true)
- }
- },
-
- // 清空
- onClearRect (clearLine) {
- this.canvasContext.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
- this.canvasContext.draw(true)
- clearLine && (this.strokes = [])
- },
-
- // 取消
- onCancel () {
- this.onClearRect(true)
- this.$emit('cancel')
- },
-
- // 保存签名 signFlag 是否已签名
- onSaveSign() {
- if (!this.strokes.length) {
- // 未签名
- this.$emit('ok', { signFlag: false })
- return
- }
- // 签名保存为图片
- uni.canvasToTempFilePath({
- canvasId: 'signCanvas',
- quality: 0.1,
- success: (res) => {
- const tempPath = res.tempFilePath
- // 然后写入另一个画布
- const signCanvas = uni.createCanvasContext('signCanvasReduce', this)
- signCanvas.translate(0, 0) // 修改原点坐标(这里是已左上角为坐标原点)
- signCanvas.drawImage(tempPath, 0, 0, 80, 160)
- signCanvas.draw(false, () => {
- setTimeout(() => {
- // 另一个画布再保存为图片,目的就是缩小图片的尺寸
- uni.canvasToTempFilePath({
- canvasId: 'signCanvasReduce',
- quality: 0.2,
- success: (res) => {
- // 清空画布
- this.onClearRect(true)
- // 转成base64
- this.imagePathToBase64(res.tempFilePath).then(base64 => {
- this.$emit('ok', { base64, signFlag: true })
- })
- },
- fail: () => {
- // toast('生成签名图片失败')
- }
- }, this)
- }, 200)
- })
- },
- fail: (res) => {
- // toast('生成签名失败')
- }
- }, this)
- },
-
- // 根据上传后的图片转成Base64格式
- imagePathToBase64(url) {
- if (!url) {
- return url
- }
- return new Promise((resolve, reject) => {
- uni.getFileSystemManager().readFile({
- filePath: url,
- encoding: 'base64',
- success: fileRes => {
- const base64 = 'data:image/png;base64,' + fileRes.data
- resolve(base64)
- },
- fail: err => {
- // toast('生成签名失败')
- resolve()
- }
- })
- })
- }
-
- }
- }
- script>
-
- <style lang="scss" scoped>
- .panel {
- width: 100%;
- position: absolute;
- left: 0;
- bottom: 0;
- right: 0;
- top: 0;
- overflow-y: hidden;
- background-color: #FFF;
- }
- .sign-canvas {
- width: 100%;
- height: 85%;
- }
- .sign-canvas-reduce {
- width: 80px;
- height: 160px;
- position: absolute;
- top: -10000rpx;
- }
- .panel-bottom {
- height: 15%;
- display: flex;
- justify-content: center;
- padding-top: 50rpx;
- }
- .panel-bottom-btn {
- transform: rotate(90deg);
- height: 40rpx;
- padding: 14rpx 36rpx;
- font-size: 30rpx;
- border-radius: 20rpx;
- color: #FFF;
- background: linear-gradient(90deg, rgba(250, 197, 22, 1), rgba(255, 141, 26, 1));
- }
- .btn-gray {
- background: #d4d4d4;
- }
- style>
- <style>
- page {
- overflow-y: hidden;
- }
- style>
继续加班了.....
码字不易,于你有利,勿忘点赞