赞
踩
调用方法
import Signature from "@/components/signature.vue"
const base64Img = ref()
//监听getSignImg
uni.$on('getSignImg', ({ base64, path }) => {
base64Img.value = base64
//console.log('签名base64, path ====>', base64, path) //拿到的图片数据
// 之后取消监听,防止重复监听
uni.$off('getSignImg')
})
<Signature :showMark="false" @cancle="cancle"></Signature>
signature.vue
<template> <view class="sign-page" v-cloak> <view class="dis-flex justify-end"> <uv-icon name="close" color="#333" size="48rpx" @click="cancle"></uv-icon> </view> <view class="sign-body"> <canvas id="signCanvas" canvas-id="signCanvas" class="sign-canvas" disable-scroll @touchstart.stop="signCanvasStart" @touchmove.stop="signCanvasMove" @touchend.stop="signCanvasEnd"></canvas> <!-- #ifndef APP --> <!--用于临时储存横屏图片的canvas容器,H5和小程序需要--> <canvas v-if="horizontal" id="hsignCanvas" canvas-id="hsignCanvas" style="position: absolute; top: -1000px; z-index: -1" :style="{ width: canvasHeight + 'px', height: canvasWidth + 'px' }"></canvas> <!-- #endif --> </view> <view class="sign-footer" :class="[horizontal ? 'horizontal-btns' : 'vertical-btns']"> <uv-button customStyle="margin-top: 20rpx;width:300rpx;height:100rpx;border-radius:20rpx;border:1px solid #3894FF" @click="reset"> <uv-icon name="shuaxin" color="#3894FF" size="48rpx" custom-prefix="custom-icon"></uv-icon> <text class="txt">重新签字</text> </uv-button> <uv-button type="primary" text="确定提交" customTextStyle="font-size:36rpx" customStyle="margin-top: 20rpx;width:300rpx;height:100rpx;border-radius:20rpx" @click="confirm"></uv-button> </view> </view> </template> <script> import { pathToBase64, base64ToPath } from '@/utils/signature.js' export default { name: 'sign', props: { // 背景水印图,优先级大于 bgColor bgImg: { type: String, default: '' }, // 背景纯色底色,为空则透明 bgColor: { type: String, default: '' }, // 是否显示水印 showMark: { type: Boolean, default: true }, // 水印内容,可多行 markText: { type: Array, default: () => { return [] // ['水印1', '水印2'] } }, // 水印样式 markStyle: { type: Object, default: () => { return { fontSize: 12, // 水印字体大小 fontFamily: 'microsoft yahei', // 水印字体 color: '#cccccc', // 水印字体颜色 rotate: 60, // 水印旋转角度 step: 2.2 // 步长,部分场景下可通过调节该参数来调整水印间距,建议为1.4-2.6左右 } } }, // 是否横屏 horizontal: { type: Boolean, default: false }, // 画笔样式 penStyle: { type: Object, default: () => { return { lineWidth: 3, // 画笔线宽 建议1~5 color: '#000000' // 画笔颜色 } } }, // 导出图片配置 expFile: { type: Object, default: () => { return { fileType: 'png', // png/jpg (png不可压缩质量,支持透明;jpg可压缩质量,不支持透明) quality: 1 // 范围 0 - 1 (仅jpg支持) } } } }, data() { return { canvasCtx: null, // canvascanvasWidth: 0, // canvas宽度 canvasWidth: 0, // canvas宽度 canvasHeight: 0, // canvas高度 x0: 0, // 初始横坐标或上一段touchmove事件中触摸点的横坐标 y0: 0, // 初始纵坐标或上一段touchmove事件中触摸点的纵坐标 signFlag: false // 签名旗帜 } }, mounted() { this.$nextTick(() => { this.createCanvas() }) }, methods: { // 创建canvas实例 createCanvas() { this.canvasCtx = uni.createCanvasContext('signCanvas', this) this.canvasCtx.setLineCap('round') // 向线条的每个末端添加圆形线帽 // 获取canvas宽高 const query = uni.createSelectorQuery().in(this) query .select('.sign-body') .boundingClientRect((data) => { this.canvasWidth = data.width this.canvasHeight = data.height }) .exec(async () => { await this.drawBg() this.drawMark(this.markText) }) }, async drawBg() { if (this.bgImg) { const img = await uni.getImageInfo({ src: this.bgImg }) this.canvasCtx.drawImage(img.path, 0, 0, this.canvasWidth, this.canvasHeight) } else if (this.bgColor) { // 绘制底色填充,否则为透明 this.canvasCtx.setFillStyle(this.bgColor) this.canvasCtx.fillRect(0, 0, this.canvasWidth, this.canvasHeight) } }, // 绘制动态水印 drawMark(textArray) { if (!this.showMark) { this.canvasCtx.draw() return } // 绘制背景 this.drawBg() // 水印参数 const markStyle = Object.assign({ fontSize: 12, // 水印字体大小 fontFamily: 'microsoft yahei', // 水印字体 color: '#cccccc', // 水印字体颜色 rotate: 60, // 水印旋转角度 step: 2 // 步长,部分场景下可通过调节该参数来调整水印间距,建议为1.4-2.6左右 }, this.markStyle ) this.canvasCtx.font = `${markStyle.fontSize}px ${markStyle.fontFamily}` this.canvasCtx.fillStyle = markStyle.color // 文字坐标 const maxPx = Math.max(this.canvasWidth / 2, this.canvasHeight / 2) const stepPx = Math.floor(maxPx / markStyle.step) let arrayX = [0] // 初始水印位置 canvas坐标 0 0 点 while (arrayX[arrayX.length - 1] < maxPx / 2) { arrayX.push(arrayX[arrayX.length - 1] + stepPx) } arrayX.push( ...arrayX.slice(1, arrayX.length).map((item) => { return -item }) ) for (let i = 0; i < arrayX.length; i++) { for (let j = 0; j < arrayX.length; j++) { this.canvasCtx.save() this.canvasCtx.translate(this.canvasWidth / 2, this.canvasHeight / 2) // 画布旋转原点 移到 图片中心 this.canvasCtx.rotate(Math.PI * (markStyle.rotate / 180)) textArray.forEach((item, index) => { let offsetY = markStyle.fontSize * index this.canvasCtx.fillText(item, arrayX[i], arrayX[j] + offsetY) }) this.canvasCtx.restore() } } this.canvasCtx.draw() }, cancle() { //取消按钮事件 this.$emit('cancle') this.reset() //uni.navigateBack() }, async reset() { this.$emit('reset') this.signFlag = false this.canvasCtx.clearRect(0, 0, this.canvasWidth, this.canvasHeight) await this.drawBg() this.drawMark(this.markText) }, async confirm() { this.$emit('confirm') // 确认按钮事件 if (!this.signFlag) { uni.showToast({ title: '请签名后再点击确定', icon: 'none', duration: 2000 }) return } uni.showModal({ title: '确认', content: '确认签名无误吗', showCancel: true, success: async ({ confirm }) => { if (confirm) { let tempFile if (this.horizontal) { tempFile = await this.saveHorizontalCanvas() } else { tempFile = await this.saveCanvas() } const base64 = await pathToBase64(tempFile) const path = await base64ToPath(base64) uni.$emit('getSignImg', { base64, path }) //uni.navigateBack() } } }) }, signCanvasEnd(e) { // 签名抬起事件 // console.log(e, 'signCanvasEnd') this.x0 = 0 this.y0 = 0 }, signCanvasMove(e) { // 签名滑动事件 // console.log(e, 'signCanvasMove') // #ifdef MP-WEIXIN let dx = e.touches[0].clientX - this.x0 let dy = e.touches[0].clientY - this.y0 // #endif // #ifndef MP-WEIXIN let dx = e.touches[0].x - this.x0 let dy = e.touches[0].y - this.y0 // #endif this.canvasCtx.moveTo(this.x0, this.y0) this.canvasCtx.lineTo(this.x0 + dx, this.y0 + dy) this.canvasCtx.setLineWidth(this.penStyle?.lineWidth || 4) this.canvasCtx.strokeStyle = this.penStyle?.color || '#000000' this.canvasCtx.stroke() this.canvasCtx.draw(true) // #ifdef MP-WEIXIN this.x0 = e.touches[0].clientX this.y0 = e.touches[0].clientY // #endif // #ifndef MP-WEIXIN this.x0 = e.touches[0].x this.y0 = e.touches[0].y // #endif }, signCanvasStart(e) { // 签名按下事件 app获取的e不一样区分小程序app // console.log('signCanvasStart', e) if (!this.signFlag) { // 导出第一次开始触碰事件 this.$emit('firstTouchStart') } this.signFlag = true // #ifdef MP-WEIXIN this.x0 = e.touches[0].clientX this.y0 = e.touches[0].clientY // #endif // #ifndef MP-WEIXIN this.x0 = e.touches[0].x this.y0 = e.touches[0].y // #endif }, // 保存竖屏图片 async saveCanvas() { return await new Promise((resolve, reject) => { uni.canvasToTempFilePath({ canvasId: 'signCanvas', fileType: this.expFile.fileType, // 只支持png和jpg quality: this.expFile.quality, // 范围 0 - 1 success: (res) => { if (!res.tempFilePath) { uni.showModal({ title: '提示', content: '保存签名失败', showCancel: false }) return } resolve(res.tempFilePath) }, fail: (r) => { console.log('图片生成失败:' + r) resolve(false) } }, this ) }) }, // 保存横屏图片 async saveHorizontalCanvas() { return await new Promise((resolve, reject) => { uni.canvasToTempFilePath({ canvasId: 'signCanvas', fileType: this.expFile.fileType, // 只支持png和jpg success: (res) => { if (!res.tempFilePath) { uni.showModal({ title: '提示', content: '保存签名失败', showCancel: false }) return } // #ifdef APP uni.compressImage({ src: res.tempFilePath, quality: this.expFile.quality * 100, // 范围 0 - 100 rotate: 270, success: (r) => { console.log('==== compressImage :', r) resolve(r.tempFilePath) } }) // #endif // #ifndef APP uni.getImageInfo({ src: res.tempFilePath, success: (r) => { // console.log('==== getImageInfo :', r) // 将signCanvas的内容复制到hsignCanvas中 const hcanvasCtx = uni.createCanvasContext( 'hsignCanvas', this) // 横屏宽高互换 hcanvasCtx.translate(this.canvasHeight / 2, this .canvasWidth / 2) hcanvasCtx.rotate(Math.PI * (-90 / 180)) hcanvasCtx.drawImage( r.path, -this.canvasWidth / 2, -this.canvasHeight / 2, this.canvasWidth, this.canvasHeight ) hcanvasCtx.draw(false, async () => { const hpathRes = await uni .canvasToTempFilePath({ canvasId: 'hsignCanvas', fileType: this.expFile .fileType, // 只支持png和jpg quality: this.expFile .quality // 范围 0 - 1 }, this ) let tempFile = '' if (Array.isArray(hpathRes)) { hpathRes.some((item) => { if (item) { tempFile = item .tempFilePath return } }) } else { tempFile = hpathRes .tempFilePath } resolve(tempFile) }) } }) // #endif }, fail: (err) => { console.log('图片生成失败:' + err) resolve(false) } }, this ) }) } } } </script> <style scoped lang="scss"> [v-cloak] { display: none !important; } .sign-page { height: 600rpx; width: 710rpx; padding: 20rpx; display: flex; flex-direction: column; .sign-body { margin-top: 50rpx; width: 100%; flex-grow: 1; background: #E5E5E5; .sign-canvas { width: 100%; height: 100%; } } .sign-footer { width: 100%; height: 80rpx; margin: 15rpx 0; display: flex; justify-content: space-evenly; align-items: center; .txt{ color:#3894FF; padding-left:10rpx; font-size: 36rpx; } } .vertical-btns { .btn { width: 120rpx; height: 66rpx; } } .horizontal-btns { .btn { width: 66rpx; height: 120rpx; writing-mode: vertical-lr; transform: rotate(90deg); } } } :deep(.uvicon-close) { font-size: 48rpx } </style>
signature.js
function getLocalFilePath(path) { if (path.indexOf('_www') === 0 || path.indexOf('_doc') === 0 || path.indexOf('_documents') === 0 || path.indexOf('_downloads') === 0) { return path } if (path.indexOf('file://') === 0) { return path } if (path.indexOf('/storage/emulated/0/') === 0) { return path } if (path.indexOf('/') === 0) { var localFilePath = plus.io.convertAbsoluteFileSystem(path) if (localFilePath !== path) { return localFilePath } else { path = path.substr(1) } } return '_www/' + path } function dataUrlToBase64(str) { var array = str.split(',') return array[array.length - 1] } var index = 0 function getNewFileId() { return Date.now() + String(index++) } function biggerThan(v1, v2) { var v1Array = v1.split('.') var v2Array = v2.split('.') var update = false for (var index = 0; index < v2Array.length; index++) { var diff = v1Array[index] - v2Array[index] if (diff !== 0) { update = diff > 0 break } } return update } export function pathToBase64(path) { return new Promise(function(resolve, reject) { if (typeof window === 'object' && 'document' in window) { if (typeof FileReader === 'function') { var xhr = new XMLHttpRequest() xhr.open('GET', path, true) xhr.responseType = 'blob' xhr.onload = function() { if (this.status === 200) { let fileReader = new FileReader() fileReader.onload = function(e) { resolve(e.target.result) } fileReader.onerror = reject fileReader.readAsDataURL(this.response) } } xhr.onerror = reject xhr.send() return } var canvas = document.createElement('canvas') var c2x = canvas.getContext('2d') var img = new Image img.onload = function() { canvas.width = img.width canvas.height = img.height c2x.drawImage(img, 0, 0) resolve(canvas.toDataURL()) canvas.height = canvas.width = 0 } img.onerror = reject img.src = path return } if (typeof plus === 'object') { plus.io.resolveLocalFileSystemURL(getLocalFilePath(path), function(entry) { entry.file(function(file) { var fileReader = new plus.io.FileReader() fileReader.onload = function(data) { resolve(data.target.result) } fileReader.onerror = function(error) { reject(error) } fileReader.readAsDataURL(file) }, function(error) { reject(error) }) }, function(error) { reject(error) }) return } if (typeof wx === 'object' && wx.canIUse('getFileSystemManager')) { wx.getFileSystemManager().readFile({ filePath: path, encoding: 'base64', success: function(res) { resolve('data:image/png;base64,' + res.data) }, fail: function(error) { reject(error) } }) return } reject(new Error('not support')) }) } export function base64ToPath(base64) { return new Promise(function(resolve, reject) { if (typeof window === 'object' && 'document' in window) { base64 = base64.split(',') var type = base64[0].match(/:(.*?);/)[1] var str = atob(base64[1]) var n = str.length var array = new Uint8Array(n) while (n--) { array[n] = str.charCodeAt(n) } return resolve((window.URL || window.webkitURL).createObjectURL(new Blob([array], { type: type }))) } var extName = base64.split(',')[0].match(/data\:\S+\/(\S+);/) if (extName) { extName = extName[1] } else { reject(new Error('base64 error')) } var fileName = getNewFileId() + '.' + extName if (typeof plus === 'object') { var basePath = '_doc' var dirPath = 'uniapp_temp' var filePath = basePath + '/' + dirPath + '/' + fileName if (!biggerThan(plus.os.name === 'Android' ? '1.9.9.80627' : '1.9.9.80472', plus.runtime.innerVersion)) { plus.io.resolveLocalFileSystemURL(basePath, function(entry) { entry.getDirectory(dirPath, { create: true, exclusive: false, }, function(entry) { entry.getFile(fileName, { create: true, exclusive: false, }, function(entry) { entry.createWriter(function(writer) { writer.onwrite = function() { resolve(filePath) } writer.onerror = reject writer.seek(0) writer.writeAsBinary(dataUrlToBase64(base64)) }, reject) }, reject) }, reject) }, reject) return } var bitmap = new plus.nativeObj.Bitmap(fileName) bitmap.loadBase64Data(base64, function() { bitmap.save(filePath, {}, function() { bitmap.clear() resolve(filePath) }, function(error) { bitmap.clear() reject(error) }) }, function(error) { bitmap.clear() reject(error) }) return } if (typeof wx === 'object' && wx.canIUse('getFileSystemManager')) { var filePath = wx.env.USER_DATA_PATH + '/' + fileName wx.getFileSystemManager().writeFile({ filePath: filePath, data: dataUrlToBase64(base64), encoding: 'base64', success: function() { resolve(filePath) }, fail: function(error) { reject(error) } }) return } reject(new Error('not support')) }) }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。