调用方法
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'))
})
}