473 lines
12 KiB
Plaintext
473 lines
12 KiB
Plaintext
|
<template>
|
|||
|
<view class="l-signature" ref="signatureRef" :style="[drawableStyle]">
|
|||
|
<!-- #ifdef APP -->
|
|||
|
<view class="l-signature-landscape" ref="signatureLandscapeRef" v-if="landscape && url !=''"
|
|||
|
:style="[landscapeStyle]">
|
|||
|
<image class="l-signature-image" :style="[landscapeImageStyle]" :src="url"></image>
|
|||
|
</view>
|
|||
|
<!-- #endif -->
|
|||
|
<!-- #ifndef APP -->
|
|||
|
<canvas id="l-signature" class="l-signature__canvas" @touchstart="touchstart" @touchmove.stop="touchmove"
|
|||
|
@touchend="touchend">
|
|||
|
</canvas>
|
|||
|
<!-- #endif -->
|
|||
|
</view>
|
|||
|
</template>
|
|||
|
<script lang="uts" setup>
|
|||
|
// @ts-nocheck
|
|||
|
// #ifdef APP
|
|||
|
import { Signature } from './signature.uts'
|
|||
|
// #endif
|
|||
|
// #ifndef APP
|
|||
|
import { Signature } from './signature.js'
|
|||
|
import { wrapEvent } from './utils'
|
|||
|
// #endif
|
|||
|
import { LSignatureToTempFilePathOptions, LSignatureToFileSuccess, LSignatureOptions } from '../../index.uts'
|
|||
|
// type SignatureToFileSuccessCallback = (res : UTSJSONObject) => void
|
|||
|
// type SignatureToFileFailCallback = (res : TakeSnapshotFail) => void
|
|||
|
// type SignatureToFileCompleteCallback = (res : any) => void
|
|||
|
|
|||
|
/**
|
|||
|
* LimeSignature 手写板签名
|
|||
|
* @description 手写板签名插件,uvue专用版。
|
|||
|
* @tutorial https://ext.dcloud.net.cn/plugin?id=4354
|
|||
|
* @property {Number} penSize 画笔大小
|
|||
|
* @property {String} penColor 画笔颜色
|
|||
|
* @property {String} backgroundColor 背景颜色,不填则为透明
|
|||
|
* @property {Boolean} disableScroll 当在写字时,禁止屏幕滚动以及下拉刷新,nvue无效
|
|||
|
*/
|
|||
|
const emit = defineEmits(['change'])
|
|||
|
const props = defineProps({
|
|||
|
styles: {
|
|||
|
type: String,
|
|||
|
default: ''
|
|||
|
},
|
|||
|
penColor: {
|
|||
|
type: String,
|
|||
|
default: 'black'
|
|||
|
},
|
|||
|
penSize: {
|
|||
|
type: Number,
|
|||
|
default: 2
|
|||
|
},
|
|||
|
backgroundColor: {
|
|||
|
type: String,
|
|||
|
default: ''
|
|||
|
},
|
|||
|
openSmooth: {
|
|||
|
type: Boolean,
|
|||
|
default: false
|
|||
|
},
|
|||
|
minLineWidth: {
|
|||
|
type: Number,
|
|||
|
default: 2
|
|||
|
},
|
|||
|
maxLineWidth: {
|
|||
|
type: Number,
|
|||
|
default: 6
|
|||
|
},
|
|||
|
minSpeed: {
|
|||
|
type: Number,
|
|||
|
default: 1.5
|
|||
|
},
|
|||
|
maxWidthDiffRate: {
|
|||
|
type: Number,
|
|||
|
default: 20
|
|||
|
},
|
|||
|
maxHistoryLength: {
|
|||
|
type: Number,
|
|||
|
default: 20
|
|||
|
},
|
|||
|
disableScroll: {
|
|||
|
type: Boolean,
|
|||
|
default: true
|
|||
|
},
|
|||
|
disabled: {
|
|||
|
type: Boolean,
|
|||
|
default: false
|
|||
|
},
|
|||
|
landscape: {
|
|||
|
type: Boolean,
|
|||
|
default: false
|
|||
|
},
|
|||
|
})
|
|||
|
|
|||
|
const drawableStyle = computed<string>(() : string => {
|
|||
|
let style : string = ''
|
|||
|
|
|||
|
if (props.backgroundColor != '') {
|
|||
|
style += `background-color: ${props.backgroundColor};`
|
|||
|
}
|
|||
|
if (props.styles != '') {
|
|||
|
style += props.styles
|
|||
|
}
|
|||
|
return style
|
|||
|
})
|
|||
|
|
|||
|
const signatureRef = ref<UniElement | null>(null)
|
|||
|
let signatureLandscapeRef = ref<UniElement | null>(null)
|
|||
|
let landscapeStyle = ref<Map<string, string>>(new Map())
|
|||
|
let landscapeImageStyle = ref<Map<string, string>>(new Map())
|
|||
|
let isCanvasEmpty = true
|
|||
|
let signature : Signature | null = null
|
|||
|
let url = ref('')
|
|||
|
// 检查并触发事件的方法
|
|||
|
const checkAndEmitEmptyStatus = () => {
|
|||
|
// #ifndef APP
|
|||
|
// @ts-ignore
|
|||
|
const isEmpty = signature?.isEmpty() ?? true
|
|||
|
// #endif
|
|||
|
// #ifdef APP
|
|||
|
// @ts-ignore
|
|||
|
const isEmpty = signature?.isEmpty ?? true
|
|||
|
// #endif
|
|||
|
if (isEmpty != isCanvasEmpty) {
|
|||
|
isCanvasEmpty = isEmpty
|
|||
|
emit('change', isCanvasEmpty)
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// #ifndef APP
|
|||
|
let canvas : HTMLCanvasElement | null = null;
|
|||
|
|
|||
|
const getTouch = (event : UniTouchEvent) => {
|
|||
|
const touch = event.touches.length ? event.touches[0] : event.changedTouches[0];
|
|||
|
return {
|
|||
|
points: [
|
|||
|
{
|
|||
|
x: touch.clientX, //- rect.left,
|
|||
|
y: touch.clientY //- rect.top
|
|||
|
}
|
|||
|
]
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
let touchstart = (e : UniTouchEvent) => {
|
|||
|
// #ifndef WEB
|
|||
|
signature!.canvas.emit('touchstart', getTouch(e))
|
|||
|
// #endif
|
|||
|
}
|
|||
|
let touchmove = (e : UniTouchEvent) => {
|
|||
|
// #ifndef WEB
|
|||
|
signature!.canvas.emit('touchmove', getTouch(e))
|
|||
|
// #endif
|
|||
|
}
|
|||
|
let touchend = (e : UniTouchEvent) => {
|
|||
|
// #ifndef WEB
|
|||
|
signature!.canvas.emit('touchend', getTouch(e))
|
|||
|
// #endif
|
|||
|
setTimeout(() => {
|
|||
|
checkAndEmitEmptyStatus()
|
|||
|
}, 0); // 绘制结束时检查
|
|||
|
}
|
|||
|
// #endif
|
|||
|
|
|||
|
const clear = () => {
|
|||
|
signature?.clear()
|
|||
|
checkAndEmitEmptyStatus() // 状态检查
|
|||
|
}
|
|||
|
const redo = () => {
|
|||
|
signature?.redo()
|
|||
|
checkAndEmitEmptyStatus() // 状态检查
|
|||
|
}
|
|||
|
const undo = () => {
|
|||
|
signature?.undo()
|
|||
|
checkAndEmitEmptyStatus() // 状态检查
|
|||
|
}
|
|||
|
const canvasToTempFilePath = (options : LSignatureToTempFilePathOptions) => {
|
|||
|
const success = options.success // as SignatureToFileSuccessCallback | null
|
|||
|
const fail = options.fail // as SignatureToFileFailCallback | null
|
|||
|
const complete = options.complete// as SignatureToFileCompleteCallback | null
|
|||
|
const format = options.format ?? 'png'
|
|||
|
// #ifdef APP
|
|||
|
signatureRef.value?.takeSnapshot({
|
|||
|
format,
|
|||
|
success: (res) => {
|
|||
|
if (props.landscape) {
|
|||
|
url.value = res.tempFilePath;
|
|||
|
setTimeout(() => {
|
|||
|
signatureLandscapeRef.value?.takeSnapshot({
|
|||
|
format,
|
|||
|
success: (res2) => {
|
|||
|
success?.({
|
|||
|
tempFilePath: res2.tempFilePath,
|
|||
|
isEmpty: signature?.isEmpty ?? false
|
|||
|
} as LSignatureToFileSuccess)
|
|||
|
}
|
|||
|
})
|
|||
|
}, 300)
|
|||
|
|
|||
|
} else {
|
|||
|
success?.({
|
|||
|
tempFilePath: res.tempFilePath,
|
|||
|
isEmpty: signature?.isEmpty ?? false
|
|||
|
} as LSignatureToFileSuccess)
|
|||
|
}
|
|||
|
},
|
|||
|
fail: (res) => {
|
|||
|
fail?.(res)
|
|||
|
},
|
|||
|
complete: (res) => {
|
|||
|
complete?.(res)
|
|||
|
}
|
|||
|
} as TakeSnapshotOptions)
|
|||
|
// #endif
|
|||
|
|
|||
|
// #ifndef APP
|
|||
|
// @ts-ignore
|
|||
|
const { backgroundColor, backgroundImage, landscape, boundingBox } = props
|
|||
|
const { quality = 1 } = options
|
|||
|
const flag = landscape || backgroundColor || boundingBox
|
|||
|
const type = `image/${format}`.replace(/jpg/, 'jpeg');
|
|||
|
const image = canvas?.toDataURL(!flag && type, !flag && quality)
|
|||
|
|
|||
|
if (flag) {
|
|||
|
// @ts-ignore
|
|||
|
// #ifdef WEB
|
|||
|
const offCanvas = document.createElement('canvas')
|
|||
|
// #endif
|
|||
|
// #ifndef WEB
|
|||
|
const offCanvas = uni.createOffscreenCanvas({ type: '2d' });
|
|||
|
// #endif
|
|||
|
// @ts-ignore
|
|||
|
const pixelRatio = signature?.canvas.get('pixelRatio')
|
|||
|
// @ts-ignore
|
|||
|
let width = signature?.canvas.get('width')
|
|||
|
// @ts-ignore
|
|||
|
let height = signature?.canvas.get('height')
|
|||
|
let x = 0
|
|||
|
let y = 0
|
|||
|
// @ts-ignore
|
|||
|
const next = () => {
|
|||
|
const size = [width, height]
|
|||
|
if (landscape) {
|
|||
|
size.reverse()
|
|||
|
}
|
|||
|
offCanvas.width = size[0] * pixelRatio
|
|||
|
offCanvas.height = size[1] * pixelRatio
|
|||
|
const param = [x, y, width, height, 0, 0, width, height].map(item => item * pixelRatio)
|
|||
|
const context = offCanvas.getContext('2d')
|
|||
|
if (landscape) {
|
|||
|
context.translate(0, width * pixelRatio)
|
|||
|
context.rotate(-Math.PI / 2)
|
|||
|
}
|
|||
|
if (backgroundColor) {
|
|||
|
context.fillStyle = backgroundColor
|
|||
|
context.fillRect(0, 0, width * pixelRatio, height * pixelRatio)
|
|||
|
}
|
|||
|
const drawImage = () => {
|
|||
|
// @ts-ignore
|
|||
|
context.drawImage(image, ...param)//signature?.canvas!.get('el')
|
|||
|
success?.({
|
|||
|
tempFilePath: offCanvas.toDataURL(type, quality),
|
|||
|
// @ts-ignore
|
|||
|
isEmpty: signature?.isEmpty() ?? false
|
|||
|
} as LSignatureToFileSuccess)
|
|||
|
offCanvas.remove()
|
|||
|
}
|
|||
|
if (backgroundImage) {
|
|||
|
const img = new Image();
|
|||
|
img.onload = () => {
|
|||
|
context.drawImage(img, ...param)
|
|||
|
drawImage()
|
|||
|
}
|
|||
|
img.src = backgroundImage
|
|||
|
}
|
|||
|
if (!backgroundImage) {
|
|||
|
drawImage()
|
|||
|
}
|
|||
|
}
|
|||
|
if (boundingBox) {
|
|||
|
// @ts-ignore
|
|||
|
const res = signature?.getContentBoundingBox()
|
|||
|
width = res.width
|
|||
|
height = res.height
|
|||
|
x = res.startX
|
|||
|
y = res.startY
|
|||
|
next()
|
|||
|
} else {
|
|||
|
next()
|
|||
|
}
|
|||
|
} else {
|
|||
|
success?.({
|
|||
|
tempFilePath: image,
|
|||
|
// @ts-ignore
|
|||
|
isEmpty: signature?.isEmpty() ?? false
|
|||
|
} as LSignatureToFileSuccess)
|
|||
|
}
|
|||
|
// #endif
|
|||
|
}
|
|||
|
|
|||
|
defineExpose({
|
|||
|
clear,
|
|||
|
redo,
|
|||
|
undo,
|
|||
|
canvasToTempFilePath,
|
|||
|
})
|
|||
|
|
|||
|
// #ifndef APP
|
|||
|
// #ifdef WEB
|
|||
|
let _touchstart, _touchmove, _touchend
|
|||
|
// #endif
|
|||
|
const instance = getCurrentInstance()!.proxy!;
|
|||
|
uni.createCanvasContextAsync({
|
|||
|
id: 'l-signature',
|
|||
|
component: instance,
|
|||
|
success: (context : CanvasContext) => {
|
|||
|
const canvasContext = context.getContext('2d')!;
|
|||
|
canvas = canvasContext.canvas;
|
|||
|
|
|||
|
// 处理高清屏逻辑
|
|||
|
const dpr = uni.getDeviceInfo().devicePixelRatio ?? 1;
|
|||
|
canvas.width = canvas.offsetWidth * dpr;
|
|||
|
canvas.height = canvas.offsetHeight * dpr;
|
|||
|
canvasContext.scale(dpr, dpr); // 仅需调用一次,当调用 reset 方法后需要再次 scale
|
|||
|
signature = new Signature({
|
|||
|
// @ts-ignore
|
|||
|
context: canvasContext,
|
|||
|
width: canvas.offsetWidth,
|
|||
|
height: canvas.offsetHeight,
|
|||
|
canvas: context,
|
|||
|
pixelRatio: 1,
|
|||
|
})
|
|||
|
// signature = new Signature({el: canvas})
|
|||
|
watchEffect(() => {
|
|||
|
const options : LSignatureOptions = {
|
|||
|
penColor: props.penColor,
|
|||
|
openSmooth: props.openSmooth,
|
|||
|
disableScroll: props.disableScroll,
|
|||
|
disabled: props.disabled,
|
|||
|
penSize: props.penSize,
|
|||
|
minLineWidth: props.minLineWidth,
|
|||
|
maxLineWidth: props.maxLineWidth,
|
|||
|
minSpeed: props.minSpeed,
|
|||
|
maxWidthDiffRate: props.maxWidthDiffRate,
|
|||
|
maxHistoryLength: props.maxHistoryLength
|
|||
|
}
|
|||
|
// @ts-ignore
|
|||
|
signature?.pen.setOption(options)
|
|||
|
})
|
|||
|
// #ifdef WEB
|
|||
|
let isTouch = false
|
|||
|
_touchstart = (event : UniMouseEvent) => {
|
|||
|
isTouch = true
|
|||
|
const rect = canvas?.getBoundingClientRect()
|
|||
|
// @ts-ignore
|
|||
|
signature!.canvas.emit('touchstart', {
|
|||
|
points: [
|
|||
|
{
|
|||
|
x: event.clientX - rect.left,
|
|||
|
y: event.clientY - rect.top
|
|||
|
}
|
|||
|
]
|
|||
|
})
|
|||
|
}
|
|||
|
_touchmove = (event : UniMouseEvent) => {
|
|||
|
if (!isTouch) return
|
|||
|
const rect = canvas?.getBoundingClientRect()
|
|||
|
// @ts-ignore
|
|||
|
signature!.canvas.emit('touchmove', {
|
|||
|
points: [
|
|||
|
{
|
|||
|
x: event.clientX - rect.left,
|
|||
|
y: event.clientY - rect.top
|
|||
|
}
|
|||
|
]
|
|||
|
})
|
|||
|
}
|
|||
|
_touchend = (event : UniMouseEvent) => {
|
|||
|
isTouch = false
|
|||
|
const rect = canvas?.getBoundingClientRect();
|
|||
|
// @ts-ignore
|
|||
|
signature!.canvas.emit('touchend', {
|
|||
|
points: [
|
|||
|
{
|
|||
|
x: event.clientX - rect.left,
|
|||
|
y: event.clientY - rect.top
|
|||
|
}
|
|||
|
]
|
|||
|
})
|
|||
|
setTimeout(function() {
|
|||
|
checkAndEmitEmptyStatus()
|
|||
|
}, 0); // 绘制结束时检查
|
|||
|
}
|
|||
|
canvas?.addEventListener('mousedown', _touchstart)
|
|||
|
canvas?.addEventListener('mousemove', _touchmove)
|
|||
|
canvas?.addEventListener('mouseup', _touchend)
|
|||
|
// canvas?.addEventListener('mouseleave', _touchend)
|
|||
|
// #endif
|
|||
|
}
|
|||
|
})
|
|||
|
// #endif
|
|||
|
|
|||
|
onMounted(() => {
|
|||
|
nextTick(() => {
|
|||
|
const width = signatureRef.value?.offsetWidth
|
|||
|
const height = signatureRef.value?.offsetHeight
|
|||
|
// #ifdef APP
|
|||
|
landscapeStyle.value.set('width', `${height}px`)
|
|||
|
landscapeStyle.value.set('height', `${width}px`)
|
|||
|
landscapeImageStyle.value.set('width', `${width}px`)
|
|||
|
landscapeImageStyle.value.set('height', `${height}px`)
|
|||
|
landscapeImageStyle.value.set('transform', `rotate(-90deg) translateY(${width}px)`)
|
|||
|
|
|||
|
signature = new Signature(signatureRef.value!)
|
|||
|
watchEffect(() => {
|
|||
|
const options : LSignatureOptions = {
|
|||
|
penColor: props.penColor,
|
|||
|
openSmooth: props.openSmooth,
|
|||
|
disableScroll: props.disableScroll,
|
|||
|
disabled: props.disabled,
|
|||
|
penSize: props.penSize,
|
|||
|
minLineWidth: props.minLineWidth,
|
|||
|
maxLineWidth: props.maxLineWidth,
|
|||
|
minSpeed: props.minSpeed,
|
|||
|
maxWidthDiffRate: props.maxWidthDiffRate,
|
|||
|
maxHistoryLength: props.maxHistoryLength
|
|||
|
}
|
|||
|
// #ifdef APP
|
|||
|
signature?.setOption(options)
|
|||
|
signature?.onChange((_isEmpty: boolean)=>{
|
|||
|
checkAndEmitEmptyStatus()
|
|||
|
})
|
|||
|
// #endif
|
|||
|
})
|
|||
|
// #endif
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
})
|
|||
|
})
|
|||
|
|
|||
|
onUnmounted(() => {
|
|||
|
// #ifdef WEB
|
|||
|
canvas?.removeEventListener('mousedown', _touchstart)
|
|||
|
canvas?.removeEventListener('mousemove', _touchmove)
|
|||
|
canvas?.removeEventListener('mouseup', _touchend)
|
|||
|
// canvas?.removeEventListener('mouseleave', touchend)
|
|||
|
canvas?.remove()
|
|||
|
// #endif
|
|||
|
|
|||
|
})
|
|||
|
</script>
|
|||
|
<style lang="scss">
|
|||
|
.l-signature {
|
|||
|
flex: 1;
|
|||
|
|
|||
|
&__canvas {
|
|||
|
width: 100%;
|
|||
|
height: 100%;
|
|||
|
}
|
|||
|
|
|||
|
&-landscape {
|
|||
|
position: absolute;
|
|||
|
pointer-events: none;
|
|||
|
left: 1000rpx;
|
|||
|
}
|
|||
|
|
|||
|
&-image {
|
|||
|
transform-origin: 0% 0%;
|
|||
|
}
|
|||
|
}
|
|||
|
</style>
|