460 lines
13 KiB
Vue
460 lines
13 KiB
Vue
<template>
|
||
<view class="sign-page">
|
||
<view class="sign-body">
|
||
<canvas
|
||
id="signCanvas"
|
||
canvas-id="signCanvas"
|
||
class="sign-canvas"
|
||
disable-scroll
|
||
@touchstart="signCanvasStart"
|
||
@touchmove="signCanvasMove"
|
||
@touchend="signCanvasEnd"
|
||
></canvas>
|
||
<!-- #ifndef APP -->
|
||
<!--用于临时储存横屏图片的canvas容器,H5和小程序需要-->
|
||
<canvas
|
||
v-if="horizontal"
|
||
id="hsignCanvas"
|
||
canvas-id="hsignCanvas"
|
||
style="position: absolute; left: -1000px; z-index: -1"
|
||
:style="{ width: canvasHeight + 'px', height: canvasWidth + 'px' }"
|
||
></canvas>
|
||
<!-- #endif -->
|
||
</view>
|
||
<!-- <view class="sign-footer" :class="[horizontal ? 'horizontal-btns' : 'vertical-btns']">-->
|
||
<!-- <view class="btn" @click="cancel">取消</view>-->
|
||
<!-- <view class="btn" @click="reset">重写</view>-->
|
||
<!-- <view class="btn" @click="confirm">确认</view>-->
|
||
<!-- </view>-->
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import { pathToBase64, base64ToPath } from './index.js'
|
||
export default {
|
||
name: 'sign',
|
||
props: {
|
||
// 签字板id,用于多签名场景下作为区分
|
||
sid: {
|
||
type: String,
|
||
default: 'sign-board'
|
||
},
|
||
// 背景水印图,优先级大于 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支持)
|
||
}
|
||
}
|
||
},
|
||
// 取消时是否需要返回
|
||
needBack: {
|
||
type: Boolean,
|
||
default: true
|
||
}
|
||
},
|
||
data() {
|
||
return {
|
||
canvasCtx: null, // canvascanvasWidth: 0, // canvas宽度
|
||
canvasWidth: 0, // canvas宽度
|
||
canvasHeight: 0, // canvas高度
|
||
x0: 0, // 初始横坐标或上一段touchmove事件中触摸点的横坐标
|
||
y0: 0, // 初始纵坐标或上一段touchmove事件中触摸点的纵坐标
|
||
signFlag: false // 签名旗帜
|
||
}
|
||
},
|
||
computed: {},
|
||
created() {},
|
||
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()
|
||
},
|
||
cancel() {
|
||
//取消按钮事件
|
||
this.$emit('cancel')
|
||
this.reset()
|
||
if (this.needBack) 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, sid: this.sid })
|
||
if (this.needBack) uni.navigateBack()
|
||
// }
|
||
// }
|
||
// })
|
||
},
|
||
signCanvasEnd(e) {
|
||
// 签名抬起事件
|
||
// console.log(e, 'signCanvasEnd')
|
||
this.x0 = 0
|
||
this.y0 = 0
|
||
},
|
||
signCanvasMove(e) {
|
||
// 签名滑动事件
|
||
// console.log(e, 'signCanvasMove')
|
||
let dx = e.touches[0].x
|
||
let dy = e.touches[0].y
|
||
|
||
this.canvasCtx.moveTo(this.x0, this.y0)
|
||
this.canvasCtx.lineTo(dx, dy)
|
||
this.canvasCtx.setLineWidth(this.penStyle?.lineWidth || 4)
|
||
this.canvasCtx.strokeStyle = this.penStyle?.color || '#000000' // 赋值过去
|
||
this.canvasCtx.stroke()
|
||
this.canvasCtx.draw(true)
|
||
|
||
this.x0 = e.touches[0].x
|
||
this.y0 = e.touches[0].y
|
||
},
|
||
signCanvasStart(e) {
|
||
// 签名按下事件 app获取的e不一样区分小程序app
|
||
// console.log('signCanvasStart', e)
|
||
if (!this.signFlag) {
|
||
// 第一次开始触碰事件
|
||
this.$emit('firstTouchStart')
|
||
}
|
||
this.signFlag = true
|
||
this.x0 = e.touches[0].x
|
||
this.y0 = e.touches[0].y
|
||
},
|
||
// 保存竖屏图片
|
||
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">
|
||
.sign-page {
|
||
height: 100%;
|
||
width: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
|
||
.sign-body {
|
||
width: 100%;
|
||
flex-grow: 1;
|
||
|
||
.sign-canvas {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
}
|
||
|
||
.sign-footer {
|
||
width: 100%;
|
||
height: 88rpx;
|
||
display: flex;
|
||
justify-content: space-evenly;
|
||
align-items: center;
|
||
border-top: 1px solid #cccccc;
|
||
box-sizing: border-box;
|
||
|
||
.btn {
|
||
line-height: 66rpx;
|
||
text-align: center;
|
||
border-radius: 12rpx;
|
||
|
||
&:nth-child(1) {
|
||
background-color: #ff0800;
|
||
color: #ffffff;
|
||
}
|
||
|
||
&:nth-child(2) {
|
||
background-color: #00d000;
|
||
color: #ffffff;
|
||
}
|
||
|
||
&:nth-child(3) {
|
||
background-color: #0184ff;
|
||
color: #ffffff;
|
||
}
|
||
}
|
||
}
|
||
|
||
.vertical-btns {
|
||
.btn {
|
||
width: 120rpx;
|
||
height: 66rpx;
|
||
}
|
||
}
|
||
|
||
.horizontal-btns {
|
||
.btn {
|
||
width: 66rpx;
|
||
height: 120rpx;
|
||
writing-mode: vertical-lr;
|
||
transform: rotate(90deg);
|
||
}
|
||
}
|
||
}
|
||
</style>
|