本文介绍了基于 uniapp3(vue3 语法)封装的手写签名组件。
包括父组件的调用方式,如通过条件判断展示签名图片或点击进入签名页面,以及接收签名照片的逻辑。子组件涵盖了自定义导航栏、清除、取消、确认等操作按钮,利用 canvas 实现手写签名功能,包括笔迹绘制、颜色选择、重写、图片旋转与导出等操作,同时涉及获取系统信息设置 canvas 尺寸和背景色等关键技术点,为在 uniapp3 项目中实现手写签名功能提供了完整的解决方案。
父组件 调用方式 :
<template>
<view style="width: 100%; padding: 0 10px">
<img class="sign-img" v-if="pageData.tempFilePath" :src="pageData.tempFilePath" />
<view class="sign-header" v-else @click="goSign">点击输入您的签名</view>
</view>
<view
style="
width: 100vw;
height: 100vh;
position: fixed;
z-index: 9999;
top: 0;
left: 0;
background-color: #fff;
"
v-if="pageData.showSign"
>
<signaturePlugin
@getTempFilePath="getTempFilePath"
@close="closeSign"
style="width: 100%; height: 100%"
/>
</view>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue'
import signaturePlugin from '@/components/signature.vue'
const pageData = reactive({
tempFilePath: '',
showSign: false
})
const closeSign = function () {
pageData.showSign = false
}
// 点击重写,进入签名页面
const goSign = function () {
pageData.showSign = true
}
// 签名页面返回回来,接收签名照片展示
const getTempFilePath = function (data) {
let { tempFilePath } = JSON.parse(data)
pageData.tempFilePath = tempFilePath
console.log('签名页面返回回来,接收签名照片展示', tempFilePath)
}
</script>
<style scope>
.sign-header {
display: flex;
justify-content: center;
align-items: center;
color: grey;
border: 1px solid #e6e6e6;
border-radius: 8px;
background-color: #fff;
height: 300rpx;
margin: 0 10px;
}
.sign-img {
border: 1px solid #e6e6e6;
border-radius: 8px;
height: 300rpx;
width: 100%;
}
</style>
子组件(手写签名组件)
<template>
<view>
<!-- 自定义导航栏 -->
<!-- <NaviBar title="签署" :autoBack="true" /> -->
<view class="wrapper">
<view class="handBtn">
<button @click="retDraw" class="delBtn">清除</button>
<button @click="saveCanvasAsImg" class="saveBtn">取消</button>
<button @click="subCanvas" class="subBtn">确认</button>
</view>
<view class="handCenter" ref="handCenter">
<canvas
class="handWriting"
:disable-scroll="true"
@touchstart="uploadScaleStart"
@touchmove="uploadScaleMove"
canvas-id="handWriting"
/>
<!--用于旋转图片的canvas容器-->
<canvas
style="position: absolute"
:style="{ width: cavWidth + 'px', height: cavWidth1 + 'px' }"
canvas-id="handWriting2"
></canvas>
</view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted, reactive, getCurrentInstance, nextTick, defineEmits } from 'vue'
const instance = getCurrentInstance()
// 定义触发的事件及其数据类型
const emit = defineEmits(['close', 'getTempFilePath'])
const canvasName = ref('handWriting')
const ctx = ref('')
const startX = ref(null)
const startY = ref(null)
const canvasWidth = ref(0)
const canvasHeight = ref(0)
const selectColor = ref('black')
const lineColor = ref('#1A1A1A') // 颜色
const canvas = ref(null)
const cavWidth = ref(1000)
const cavWidth1 = ref(1000)
const lineSize = ref(5) // 笔记倍数
const location = ref(null)
const handCenter = ref(null)
onMounted(() => {
ctx.value = uni.createCanvasContext('handWriting', instance.proxy)
uni.getSystemInfo({
success: function (res) {
const windowWidth = res.windowWidth // 窗口宽度
const windowHeight = res.windowHeight // 窗口高度
console.log(windowWidth)
console.log(windowHeight)
cavWidth.value = canvasWidth.value = windowWidth
cavWidth1.value = canvasHeight.value = windowHeight
setCanvasBg('#fff')
},
})
})
// 笔迹开始
const uploadScaleStart = (e) => {
startX.value = e.changedTouches[0].x
startY.value = e.changedTouches[0].y
//设置画笔参数
//画笔颜色
ctx.value.setStrokeStyle(lineColor.value)
//设置线条粗细
ctx.value.setLineWidth(lineSize.value)
//设置线条的结束端点样式
ctx.value.setLineCap('round') //'butt'、'round'、'square'
//开始画笔
ctx.value.beginPath()
}
// 笔迹移动
const uploadScaleMove = (e) => {
//取点
let temX = e.changedTouches[0].x
let temY = e.changedTouches[0].y
//画线条
ctx.value.moveTo(startX.value, startY.value)
ctx.value.lineTo(temX, temY)
ctx.value.stroke()
startX.value = temX
startY.value = temY
ctx.value.draw(true)
}
// 重写
const retDraw = () => {
ctx.value.clearRect(0, 0, 700, 730)
ctx.value.draw()
//设置canvas背景
setCanvasBg('#fff')
}
// 选择颜色
const selectColorEvent = (str, color) => {
selectColor.value = str
lineColor.value = color
}
// 确认
const subCanvas = () => {
uni.canvasToTempFilePath(
{
canvasId: 'handWriting',
fileType: 'png',
quality: 1, //图片质量
success: (res) => {
console.log(res.tempFilePath, 'canvas生成图片地址')
wx.getImageInfo({
// 获取图片的信息
src: res.tempFilePath,
success: (res1) => {
console.log('res1', res1)
// 将canvas1的内容复制到canvas2中
let canvasContext = uni.createCanvasContext('handWriting2', instance)
let rate = res1.height / res1.width
let width = 300 / rate
let height = 300
cavWidth.value = height
cavWidth1.value = width
canvasContext.translate(height / 2, width / 2)
canvasContext.rotate((270 * Math.PI) / 180)
canvasContext.drawImage(res.tempFilePath, -width / 2, -height / 2, width, height)
console.log(0, canvasContext)
canvasContext.draw(false, () => {
// 将之前在绘图上下文中的描述(路径、变形、样式)画到 canvas 中
uni.canvasToTempFilePath(
{
// 把当前画布指定区域的内容导出生成指定大小的图片。在 draw() 回调里调用该方法才能保证图片导出成功。
canvasId: 'handWriting2',
fileType: 'png',
quality: 1, //图片质量
success: (res2) => {
console.log('res2', res2)
let data = JSON.stringify({
tempFilePath: res2.tempFilePath,
})
emit('getTempFilePath', data)
emit('close')
},
},
instance,
)
})
},
})
},
},
instance,
)
}
//旋转图片,生成新canvas实例
const rotate = (cb) => {
wx.createSelectorQuery()
.select('#handWriting2')
.fields({
node: true,
size: true,
})
.exec((res) => {
const rotateCanvas = res[0].node
const rotateCtx = rotateCanvas.getContext('2d')
//this.ctxW-->所绘制canvas的width
//this.ctxH -->所绘制canvas的height
rotateCanvas.width = canvasHeight.value
rotateCanvas.height = canvasWidth.value
wx.canvasToTempFilePath(
{
canvas: canvas.value,
success: (res) => {
const img = rotateCanvas.createImage()
img.src = res.tempFilePath
img.onload = function () {
rotateCtx.translate(rotateCanvas.width / 2, rotateCanvas.height / 2)
rotateCtx.rotate((270 * Math.PI) / 180)
rotateCtx.drawImage(img, -rotateCanvas.height / 2, -rotateCanvas.width / 2)
rotateCtx.scale(1, 1)
cb(rotateCanvas)
}
},
fail: (err) => {
console.log(err)
},
},
instance,
)
})
}
//取消
const saveCanvasAsImg = () => {
retDraw()
// uni.navigateBack()
emit('close')
}
//设置canvas背景色 不设置 导出的canvas的背景为透明
//@params:字符串 color
const setCanvasBg = (color) => {
/* 将canvas背景设置为 白底,不设置 导出的canvas的背景为透明 */
//rect() 参数说明 矩形路径左上角的横坐标,左上角的纵坐标, 矩形路径的宽度, 矩形路径的高度
//这里是 canvasHeight - 4 是因为下边盖住边框了,所以手动减了写
ctx.value.rect(0, 0, canvasWidth.value, canvasHeight.value - 4)
ctx.value.setFillStyle(color)
ctx.value.fill() //设置填充
ctx.value.draw() //开画
}
</script>
<style>
page {
background: #fbfbfb;
height: auto;
overflow: hidden;
}
.wrapper {
position: relative;
width: 100%;
height: 100vh;
margin: 20rpx 0;
overflow: hidden;
display: flex;
align-content: center;
flex-direction: row;
justify-content: center;
font-size: 28rpx;
}
.handWriting {
background: #fff;
width: 100%;
height: 100vh;
}
.handCenter {
border-left: 2rpx solid #e9e9e9;
flex: 5;
overflow: hidden;
box-sizing: border-box;
}
.handBtn button {
font-size: 28rpx;
}
.handBtn {
height: 100vh;
display: inline-flex;
flex-direction: column;
justify-content: space-between;
align-content: space-between;
flex: 1;
}
.delBtn {
width: 200rpx;
position: absolute;
bottom: 350rpx;
left: -35rpx;
transform: rotate(90deg);
color: #666;
}
.subBtn {
width: 200rpx;
position: absolute;
bottom: 52rpx;
left: -35rpx;
display: inline-flex;
transform: rotate(90deg);
background: #29cea0;
color: #fff;
margin-bottom: 60rpx;
text-align: center;
justify-content: center;
}
/*Peach - 新增 - 保存*/
.saveBtn {
width: 200rpx;
position: absolute;
bottom: 590rpx;
left: -35rpx;
transform: rotate(90deg);
color: #666;
}
</style>