初始化
This commit is contained in:
24
uni_modules/jp-signature/changelog.md
Normal file
24
uni_modules/jp-signature/changelog.md
Normal file
@@ -0,0 +1,24 @@
|
||||
## 3.2.0(2024-08-27)
|
||||
修复全屏签名图片未正确旋转问题
|
||||
## 3.1.0(2024-06-03)
|
||||
修复签名部分BUG
|
||||
## 3.0.4(2024-05-28)
|
||||
修改部分文档内容
|
||||
## 3.0.0(2024-05-28)
|
||||
新增jp-merge组件,及修复jp-signature-popup组件签字滚动问题
|
||||
## 2.5.2(2023-12-19)
|
||||
弹窗签名新增boundingBox属性,文档新增vue3使用方法
|
||||
## 2.5.1(2023-11-17)
|
||||
修复vue3兼容问题,新增点击图片除非事件
|
||||
## 2.5(2023-11-14)
|
||||
优化代码部分能力
|
||||
## 2.4(2023-11-14)
|
||||
新增弹框签名笔锋配置
|
||||
## 2.3(2023-11-14)
|
||||
修复滚动无法签名问题,弹框签名样式改变
|
||||
## 2.2(2023-11-14)
|
||||
修复app滚动无法签名等问题,修改了弹框签名样式
|
||||
## 2.1(2023-08-15)
|
||||
修改弹窗高度,修改实例项目
|
||||
## 2.0(2023-08-15)
|
||||
修复签名不兼容问题,改变签名方式,使用uni_modules方式
|
||||
103
uni_modules/jp-signature/components/jp-merge/jp-merge.vue
Normal file
103
uni_modules/jp-signature/components/jp-merge/jp-merge.vue
Normal file
@@ -0,0 +1,103 @@
|
||||
<template>
|
||||
<view class="share">
|
||||
<canvas
|
||||
canvas-id="shareCanvas"
|
||||
class="canvas"
|
||||
bindlongpress="saveImg"
|
||||
catchtouchmove="true"
|
||||
style="position:fixed;left:500%"
|
||||
:style="{height: canvasHeight+'px',width:canvasWidth+'px'}"
|
||||
>
|
||||
</canvas>
|
||||
</view>
|
||||
</template>
|
||||
<!-- 有项目需要开发的请联系 扣 - 371524845 -->
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
canvasHeight: {
|
||||
type: Number,
|
||||
default: 400,
|
||||
},
|
||||
canvasWidth: {
|
||||
type: Number,
|
||||
default: 400,
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
default: 80,
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
default: 50,
|
||||
},
|
||||
left: {
|
||||
type: Number,
|
||||
default: 300,
|
||||
},
|
||||
top: {
|
||||
type: Number,
|
||||
default: 320,
|
||||
},
|
||||
bgImage: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data(){
|
||||
return {
|
||||
ctx:null
|
||||
}
|
||||
},
|
||||
created() {
|
||||
//初始化画布
|
||||
this.ctx = wx.createCanvasContext('shareCanvas',this)
|
||||
},
|
||||
methods:{
|
||||
//获取图片的基本信息,即将网络图片转成本地图片,
|
||||
getImageInfo(src) {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.getImageInfo({
|
||||
src,
|
||||
success: (res) => resolve(res),
|
||||
fail: (res) => reject(res)
|
||||
})
|
||||
});
|
||||
},
|
||||
exportPost(image2){
|
||||
let that = this
|
||||
return new Promise(function (resolve, reject) {
|
||||
let image = that.bgImage
|
||||
//获取系统的基本信息,为后期的画布和底图适配宽高
|
||||
uni.getSystemInfo({
|
||||
success: function (res) {
|
||||
Promise.all([that.getImageInfo(image),that.getImageInfo(image2)]).then(res=>{
|
||||
//获取底图和二维码图片的基本信息,通常前端导出的二维码是base64格式的,所以要转成图片格式的才可以获取图片的基本信息
|
||||
that.ctx.drawImage(res[0].path,0 , 0,that.canvasWidth,that.canvasHeight);
|
||||
that.ctx.drawImage(res[1].path,that.left,that.top,that.width, that.height);
|
||||
that.ctx.draw(false,function(){
|
||||
wx.canvasToTempFilePath({
|
||||
x: 0,
|
||||
y: 0,
|
||||
width:that.canvasWidth,
|
||||
height:that.canvasHeight,
|
||||
destWidth:that.canvasWidth*2,//这里乘以2是为了保证合成图片的清晰度
|
||||
destHeight:that.canvasHeight*2,
|
||||
canvasId: 'shareCanvas',
|
||||
fileType:'jpg',//设置导出图片的后缀名
|
||||
success: function (res) {
|
||||
resolve(res.tempFilePath)
|
||||
},
|
||||
fail: function (res) {
|
||||
reject(res)
|
||||
},
|
||||
})
|
||||
});
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,328 @@
|
||||
<template>
|
||||
<div class="signature">
|
||||
<div class="inputs" v-if="!popup">
|
||||
<div class="label" :class="required?'labelqr':''">{{label}}</div>
|
||||
<div>
|
||||
<div v-if="value" class="images">
|
||||
<image @tap="toImg" class="images" mode="aspectFit" :src="value"></image>
|
||||
<view v-if="!readonly" @click="toDeleteImg" class="icons">
|
||||
<view class="Deletes">×</view>
|
||||
</view>
|
||||
</div>
|
||||
<div v-if="!value && !readonly" class="explain" @click="toPop">
|
||||
{{placeholder?placeholder:'点击签名'}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<view class="bottomPopup" v-if="showPopup" @touchmove.stop.prevent="moveHandle">
|
||||
<transition name="slide-up" appear>
|
||||
<view class="popup-content">
|
||||
<view class="popup">
|
||||
<div class="hader" v-if="!isHeight">
|
||||
<div @click="toclear">取消</div>
|
||||
<div class="text">{{label}}</div>
|
||||
<div @click="isEmpty">确定</div>
|
||||
</div>
|
||||
<div :class="isHeight?'wgSignatureq':'wgSignature'">
|
||||
<div v-if="isHeight" key="999" style="width: 750rpx ;height: 100vh;">
|
||||
<jp-signature :beforeDelay="200" :landscape="true" disableScroll ref="signatureRef" :openSmooth="openSmooth" :penSize="6" :bounding-box="boundingBox"></jp-signature>
|
||||
</div>
|
||||
<div v-else key="888" style="width: 750rpx ;height: 35vh;">
|
||||
<jp-signature :beforeDelay="200" disableScroll ref="signatureRef" :openSmooth="openSmooth" :bounding-box="boundingBox" :penSize="3"></jp-signature>
|
||||
</div>
|
||||
<div v-if="!isHeight" class="appBut" >
|
||||
<div class="buts" @click="undo" >撤销</div>
|
||||
<div class="buts" @click="deleteImg" >清除</div>
|
||||
<div class="buts" style="background-color: #55aaff;color: #fff;" @click="Tomagnify" >全屏</div>
|
||||
</div>
|
||||
<div v-else class="appBut" style="height: 80px;">
|
||||
<div class="butx" @click="undo" >撤销</div>
|
||||
<div class="butx" @click="deleteImg">清除</div>
|
||||
<div class="butx" style="background-color: #55aaff;color: #fff;" @click="Tomagnify" >小屏</div>
|
||||
<div class="butx" @click="toclear">取消</div>
|
||||
<div class="butx" style="background-color: #E59C36;color: #fff;" @click="isEmpty">完成</div>
|
||||
</div>
|
||||
</div>
|
||||
</view>
|
||||
</view>
|
||||
</transition>
|
||||
</view>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
<!-- 有项目需要开发的请联系 扣 - 371524845 -->
|
||||
<script>
|
||||
/**
|
||||
* 手写签名组件
|
||||
* 用于手写签名(弹框签名支持小屏和全屏)
|
||||
*
|
||||
*********参数********
|
||||
* label 选项名称
|
||||
* value 初始值String(支持bas64,url 等图片显示)
|
||||
* required 是否显示必填
|
||||
* placeholder 默认值
|
||||
* readonly 是否只读
|
||||
*
|
||||
* *********回调********
|
||||
* @input(e) 点击确认 e生成的图片数据(bas64)
|
||||
*
|
||||
*********方法********
|
||||
* isEmpty() 生成图片
|
||||
* deleteImg() 删除图片
|
||||
*/
|
||||
export default {
|
||||
props: {
|
||||
popup: {
|
||||
type: [Boolean, String],
|
||||
default: false,
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: '手写签名',
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
required: {
|
||||
type: [Boolean, String],
|
||||
default: false,
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '点击签名',
|
||||
},
|
||||
readonly: {
|
||||
type: [Boolean, String],
|
||||
default: false,
|
||||
},
|
||||
openSmooth: {
|
||||
type: [Boolean, String],
|
||||
default: true,
|
||||
},
|
||||
boundingBox: {
|
||||
type: [Boolean, String],
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showPopup: false,
|
||||
isHeight: false,
|
||||
height1: uni.getSystemInfoSync().windowWidth / 2,
|
||||
width: uni.getSystemInfoSync().windowWidth, //实时屏幕宽度
|
||||
height: uni.getSystemInfoSync().windowHeight, //实时屏幕高度
|
||||
showPicker: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
moveHandle(){
|
||||
|
||||
},
|
||||
toImg(){
|
||||
this.$emit('toImg',this.value)
|
||||
},
|
||||
undo() {
|
||||
this.$refs.signatureRef.undo()
|
||||
},
|
||||
toPop() {
|
||||
this.showPopup = true
|
||||
},
|
||||
toDeleteImg() {
|
||||
// #ifndef VUE3
|
||||
this.$emit('input','')
|
||||
// #endif
|
||||
// #ifdef VUE3
|
||||
this.$emit('update:value','')
|
||||
// #endif
|
||||
},
|
||||
toclear() {
|
||||
this.isHeight = false
|
||||
this.showPopup = false
|
||||
},
|
||||
close() {
|
||||
this.isHeight = false
|
||||
this.showPopup = false
|
||||
const {signatureRef} = this.$refs
|
||||
signatureRef.clear()
|
||||
},
|
||||
deleteImg() {
|
||||
const {signatureRef} = this.$refs
|
||||
signatureRef.clear()
|
||||
},
|
||||
toDataURL(url) {
|
||||
// #ifndef VUE3
|
||||
this.$emit('input',url)
|
||||
// #endif
|
||||
// #ifdef VUE3
|
||||
this.$emit('update:value',url)
|
||||
// #endif
|
||||
this.showPicker = false
|
||||
},
|
||||
Tomagnify() {
|
||||
this.isHeight = !this.isHeight
|
||||
const {signatureRef} = this.$refs
|
||||
signatureRef.clear()
|
||||
},
|
||||
isEmpty() {
|
||||
const {signatureRef} = this.$refs
|
||||
signatureRef.canvasToTempFilePath({
|
||||
quality: 0.8,
|
||||
success: (res) => {
|
||||
if (this.required) {
|
||||
if (!res.isEmpty) {
|
||||
// #ifndef VUE3
|
||||
this.$emit('input', res.tempFilePath)
|
||||
// #endif
|
||||
// #ifdef VUE3
|
||||
this.$emit('update:value',res.tempFilePath)
|
||||
// #endif
|
||||
this.$emit('change', res.tempFilePath)
|
||||
this.isHeight = false
|
||||
this.showPopup = false
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '请先签名',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// #ifndef VUE3
|
||||
this.$emit('input', res.tempFilePath)
|
||||
// #endif
|
||||
// #ifdef VUE3
|
||||
this.$emit('update:value',res.tempFilePath)
|
||||
// #endif
|
||||
this.$emit('change', res.tempFilePath)
|
||||
this.isHeight = false
|
||||
this.showPopup = false
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
beforeCreate() {},
|
||||
created() {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.wgSignatureq{
|
||||
|
||||
}
|
||||
.appBut{
|
||||
display: flex;justify-content: flex-start;align-items: center;text-align: center;height: 50px;line-height: 35px;
|
||||
.buts{
|
||||
color: #333;flex: 1;margin: 0 15px;background-color: #ccc;border-radius: 5px;height: 35px;
|
||||
}
|
||||
.butx{
|
||||
color: #333;flex: 1;margin: 0 5px;background-color: #ccc;border-radius: 5px;height: 35px;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
.bottomPopup {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
z-index: 999;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
|
||||
.popup-content {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
// top: 0;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.slide-up-enter-active,
|
||||
.slide-up-leave-active {
|
||||
transition: all .3s ease;
|
||||
}
|
||||
|
||||
.slide-up-enter,
|
||||
.slide-up-leave-to {
|
||||
transform: translateY(100%);
|
||||
}
|
||||
}
|
||||
|
||||
.signature {
|
||||
.inputs {
|
||||
background-color: #fff;
|
||||
padding: 10px 16px;
|
||||
|
||||
.label {
|
||||
line-height: 35px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.labelqr:before {
|
||||
content: "*";
|
||||
color: #f00;
|
||||
}
|
||||
|
||||
.explain {
|
||||
width: 100%;
|
||||
background-color: #f1f1f1;
|
||||
text-align: center;
|
||||
line-height: 40px;
|
||||
border: 1px dotted #ccc;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.Deletes {
|
||||
border: 1px solid #f00;
|
||||
width: 30rpx;
|
||||
height: 30rpx;
|
||||
border-radius: 50%;
|
||||
color: #f00;
|
||||
text-align: center;
|
||||
font-size: 30rpx;
|
||||
line-height: 30rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.images {
|
||||
width: 300rpx;
|
||||
height: 150rpx;
|
||||
position: relative;
|
||||
|
||||
.icons {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.popup {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.hader {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
height: 45px;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
align-items: center;
|
||||
|
||||
div {
|
||||
text-align: center;
|
||||
width: 80px;
|
||||
color: #E59C36;
|
||||
}
|
||||
|
||||
.text {
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
199
uni_modules/jp-signature/components/jp-signature/context.js
Normal file
199
uni_modules/jp-signature/components/jp-signature/context.js
Normal file
@@ -0,0 +1,199 @@
|
||||
const uniPlatform = uni.getSystemInfoSync().uniPlatform
|
||||
|
||||
export const uniContext = (canvasId, context) => {
|
||||
let ctx = uni.createCanvasContext(canvasId, context)
|
||||
if (!ctx.uniDrawImage) {
|
||||
ctx.uniDrawImage = ctx.drawImage
|
||||
ctx.drawImage = (image, ...agrs) => {
|
||||
ctx.uniDrawImage(image.src, ...agrs)
|
||||
}
|
||||
}
|
||||
|
||||
if (!ctx.getImageData) {
|
||||
ctx.getImageData = (x, y, width, height) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// #ifdef MP || VUE2
|
||||
if (context.proxy) context = context.proxy
|
||||
// #endif
|
||||
uni.canvasGetImageData({
|
||||
canvasId,
|
||||
x,
|
||||
y,
|
||||
width:parseInt(width),
|
||||
height:parseInt(height),
|
||||
success(res) {
|
||||
resolve(res)
|
||||
},
|
||||
fail(error) {
|
||||
reject(error)
|
||||
}
|
||||
}, context)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
ctx._getImageData = ctx.getImageData
|
||||
ctx.getImageData = (x, y, width, height) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
ctx._getImageData({
|
||||
x,
|
||||
y,
|
||||
width: parseInt(width) ,
|
||||
height:parseInt(height),
|
||||
success(res) {
|
||||
resolve(res)
|
||||
},
|
||||
fail(error) {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
||||
class Image {
|
||||
constructor() {
|
||||
this.currentSrc = null
|
||||
this.naturalHeight = 0
|
||||
this.naturalWidth = 0
|
||||
this.width = 0
|
||||
this.height = 0
|
||||
this.tagName = 'IMG'
|
||||
}
|
||||
onerror() {}
|
||||
onload() {}
|
||||
set src(src) {
|
||||
this.currentSrc = src
|
||||
uni.getImageInfo({
|
||||
src,
|
||||
success: (res) => {
|
||||
this.naturalWidth = this.width = res.width
|
||||
this.naturalHeight = this.height = res.height
|
||||
this.onload()
|
||||
},
|
||||
fail: () => {
|
||||
this.onerror()
|
||||
}
|
||||
})
|
||||
}
|
||||
get src() {
|
||||
return this.currentSrc
|
||||
}
|
||||
}
|
||||
|
||||
export const createImage = () => {
|
||||
return new Image()
|
||||
}
|
||||
export function useCurrentPage() {
|
||||
const pages = getCurrentPages();
|
||||
return pages[pages.length - 1];
|
||||
}
|
||||
export const toDataURL = (canvasId, context, options = {}) => {
|
||||
// #ifdef MP-QQ
|
||||
// context = context.$scope
|
||||
// #endif
|
||||
// #ifdef MP-ALIPAY
|
||||
context = ''
|
||||
// #endif
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let {
|
||||
canvas,
|
||||
width,
|
||||
height,
|
||||
destWidth = 0,
|
||||
destHeight = 0,
|
||||
x = 0,
|
||||
y = 0,
|
||||
preferToDataURL
|
||||
} = options
|
||||
const {
|
||||
pixelRatio
|
||||
} = uni.getSystemInfoSync()
|
||||
|
||||
// #ifdef MP-ALIPAY
|
||||
const isDD = typeof dd != 'undefined'
|
||||
if (!isDD && (!destWidth || !destHeight)) {
|
||||
destWidth = width * pixelRatio;
|
||||
destHeight = height * pixelRatio;
|
||||
width = destWidth;
|
||||
height = destHeight;
|
||||
x = x * pixelRatio
|
||||
y = y * pixelRatio
|
||||
}
|
||||
// #endif
|
||||
const params = {
|
||||
...options,
|
||||
canvasId,
|
||||
id: canvasId,
|
||||
// #ifdef MP-ALIPAY
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
destWidth,
|
||||
destHeight,
|
||||
// #endif
|
||||
canvas,
|
||||
success: (res) => {
|
||||
resolve(res.tempFilePath)
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err)
|
||||
}
|
||||
}
|
||||
// 抖音小程序canvas 2d不支持canvasToTempFilePath
|
||||
if (canvas && canvas.toDataURL && preferToDataURL) {
|
||||
let next = true
|
||||
const devtools = uni.getSystemInfoSync().platform == 'devtools'
|
||||
// #ifdef MP-TOUTIAO
|
||||
next = uni.getSystemInfoSync().platform != 'devtools'
|
||||
if (!next) {
|
||||
console.warn('[lime-signature] 抖音开发工具不支持bbox')
|
||||
}
|
||||
// #endif
|
||||
if ((x || y) && next) {
|
||||
const offCanvas = uni.createOffscreenCanvas({
|
||||
type: '2d'
|
||||
});
|
||||
const ctx = offCanvas.getContext("2d");
|
||||
const destWidth = Math.floor(width * pixelRatio)
|
||||
const destHeight = Math.floor(height * pixelRatio)
|
||||
offCanvas.width = destWidth // canvas.width;
|
||||
offCanvas.height = destHeight // canvas.height;
|
||||
// ctx.scale(pixelRatio, pixelRatio)
|
||||
// ctx.drawImage(canvas, Math.floor(x*pixelRatio), Math.floor(y*pixelRatio), destWidth, destHeight, 0,0, destWidth, destHeight);
|
||||
// 抖音不能在drawImage使用canvas
|
||||
const image = canvas.createImage()
|
||||
image.onload = () => {
|
||||
ctx.drawImage(image, Math.floor(x * pixelRatio), Math.floor(y * pixelRatio),
|
||||
destWidth, destHeight, 0, 0, destWidth, destHeight)
|
||||
const tempFilePath = offCanvas.toDataURL();
|
||||
resolve(tempFilePath)
|
||||
if (params.success) {
|
||||
params.success({
|
||||
tempFilePath
|
||||
})
|
||||
}
|
||||
}
|
||||
image.src = canvas.toDataURL()
|
||||
|
||||
} else {
|
||||
const tempFilePath = canvas.toDataURL()
|
||||
resolve(tempFilePath)
|
||||
if (params.success) {
|
||||
params.success({
|
||||
tempFilePath
|
||||
})
|
||||
}
|
||||
}
|
||||
} else if (canvas && canvas.toTempFilePath) {
|
||||
canvas.toTempFilePath(params)
|
||||
} else {
|
||||
uni.canvasToTempFilePath(params, context)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
@@ -0,0 +1,364 @@
|
||||
<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 -->
|
||||
<!-- #ifdef WEB -->
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</template>
|
||||
<script lang="uts" setup>
|
||||
// @ts-nocheck
|
||||
// #ifdef APP
|
||||
import { Signature } from './signature.uts'
|
||||
// #endif
|
||||
// #ifndef APP
|
||||
import { Signature } from './signature.js'
|
||||
// #endif
|
||||
import { nextTick } from 'vue'
|
||||
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 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 signature : Signature | null = null
|
||||
let url = ref('')
|
||||
// #ifdef WEB
|
||||
let canvas : HTMLCanvasElement | null = null
|
||||
let touchstart,touchmove,touchend
|
||||
// #endif
|
||||
|
||||
const clear = () => {
|
||||
signature?.clear()
|
||||
}
|
||||
const redo = () => {
|
||||
signature?.redo()
|
||||
}
|
||||
const undo = () => {
|
||||
signature?.undo()
|
||||
}
|
||||
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
|
||||
|
||||
// #ifdef WEB
|
||||
// @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
|
||||
const canvas = document.createElement('canvas')
|
||||
// @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()
|
||||
}
|
||||
canvas.width = size[0] * pixelRatio
|
||||
canvas.height = size[1] * pixelRatio
|
||||
const param = [x, y, width, height, 0, 0, width, height].map(item => item * pixelRatio)
|
||||
const context = canvas.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(signature?.canvas!.get('el'), ...param)
|
||||
success?.({
|
||||
tempFilePath: canvas.toDataURL(type, quality),
|
||||
// @ts-ignore
|
||||
isEmpty: signature?.isEmpty() ?? false
|
||||
} as LSignatureToFileSuccess)
|
||||
canvas.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,
|
||||
})
|
||||
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!)
|
||||
// #endif
|
||||
// #ifdef WEB
|
||||
canvas = document.createElement('canvas')
|
||||
canvas!.style = 'width: 100%; height: 100%;'
|
||||
signatureRef.value?.appendChild(canvas as UniElement)
|
||||
// @ts-ignore
|
||||
signature = new Signature({ el: canvas })
|
||||
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
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
canvas?.addEventListener('mousedown', touchstart)
|
||||
canvas?.addEventListener('mousemove', touchmove)
|
||||
canvas?.addEventListener('mouseup', touchend)
|
||||
canvas?.addEventListener('mouseleave', touchend)
|
||||
|
||||
|
||||
// #endif
|
||||
|
||||
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)
|
||||
// #endif
|
||||
// #ifdef WEB
|
||||
// @ts-ignore
|
||||
signature?.pen.setOption(options)
|
||||
// #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;
|
||||
|
||||
&-landscape {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
left: 1000rpx;
|
||||
}
|
||||
|
||||
&-image {
|
||||
transform-origin: 0% 0%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,728 @@
|
||||
<template>
|
||||
<view class="lime-signature" v-if="show" :style="[canvasStyle, styles]" ref="limeSignature">
|
||||
<!-- #ifndef APP-VUE || APP-NVUE -->
|
||||
<canvas v-if="useCanvas2d" class="lime-signature__canvas" :id="canvasId" type="2d"
|
||||
:disableScroll="disableScroll" @touchstart="touchStart" @touchmove="touchMove"
|
||||
@touchend="touchEnd"></canvas>
|
||||
<canvas v-else :disableScroll="disableScroll" class="lime-signature__canvas" :canvas-id="canvasId"
|
||||
:id="canvasId" :width="canvasWidth" :height="canvasHeight" @touchstart="touchStart" @touchmove="touchMove"
|
||||
@touchend="touchEnd" @mousedown="touchStart" @mousemove="touchMove" @mouseup="touchEnd"></canvas>
|
||||
<canvas v-if="showOffscreen" class="offscreen" canvas-id="offscreen" id="offscreen"
|
||||
:style="'width:' + offscreenSize[0] + 'px;height:' + offscreenSize[1] + 'px'" :width="offscreenSize[0]"
|
||||
:height="offscreenSize[1]">
|
||||
</canvas>
|
||||
<view v-if="showMask" class="mask" @touchstart="touchStart" @touchmove.stop.prevent="touchMove"
|
||||
@touchend="touchEnd"></view>
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef APP-VUE -->
|
||||
<view :id="canvasId" :disableScroll="disableScroll" :rparam="param" :change:rparam="sign.update"
|
||||
:rclear="rclear" :change:rclear="sign.clear" :rundo="rundo" :rredo="rredo" :change:rredo="sign.redo"
|
||||
:change:rundo="sign.undo" :rsave="rsave" :rmask="rmask" :change:rsave="sign.save" :change:rmask="sign.mask"
|
||||
:rdestroy="rdestroy" :change:rdestroy="sign.destroy" :rempty="rempty" :change:rempty="sign.isEmpty">
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef APP-NVUE -->
|
||||
<web-view src="/uni_modules/lime-signature/hybrid/html/index.html" class="lime-signature__canvas" ref="webview"
|
||||
@pagefinish="onPageFinish" @error="onError" @onPostMessage="onMessage"></web-view>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// #ifndef APP-NVUE
|
||||
import {
|
||||
canIUseCanvas2d,
|
||||
wrapEvent,
|
||||
requestAnimationFrame,
|
||||
sleep,
|
||||
isTransparent
|
||||
} from './utils'
|
||||
import {
|
||||
Signature
|
||||
} from './signature.js'
|
||||
// import {Signature} from '@signature';
|
||||
import {
|
||||
uniContext,
|
||||
createImage,
|
||||
toDataURL
|
||||
} from './context'
|
||||
// #endif
|
||||
import props from './props';
|
||||
import {
|
||||
base64ToPath,
|
||||
getRect
|
||||
} from './utils'
|
||||
import { nextTick } from 'vue';
|
||||
|
||||
/**
|
||||
* LimeSignature 手写板签名
|
||||
* @description 手写板签名插件:一款能跑在uniapp各端中的签名插件,支持横屏、背景色、笔画颜色、笔画大小等功能,可生成有内容的区域,减小图片尺寸,节省空间。
|
||||
* @property {Number} penSize 画笔大小
|
||||
* @property {Number} minLineWidth 线条最小宽
|
||||
* @property {Number} maxLineWidth 线条最大宽
|
||||
* @property {String} penColor 画笔颜色
|
||||
* @property {String} backgroundColor 背景颜色,不填则为透明
|
||||
* @property {type} 指定 canvas 类型
|
||||
* @value 2d canvas 2d
|
||||
* @value '' 非 canvas 2d 旧接口,微信不再维护
|
||||
* @property {Boolean} openSmooth 模拟笔锋
|
||||
* @property {Number} beforeDelay 延时初始化,在放在弹窗里可以使用 (毫秒)
|
||||
* @property {Number} maxHistoryLength 限制历史记录数,即最大可撤销数,传入0则关闭历史记录功能
|
||||
* @property {Boolean} landscape 横屏,使用后在最后生成图片时会图片旋转90度
|
||||
* @property {Boolean} disableScroll 当在写字时,禁止屏幕滚动以及下拉刷新,nvue无效
|
||||
* @property {Boolean} boundingBox 只生成内容区域,即未画部分不生成,有性能的损耗
|
||||
*/
|
||||
export default {
|
||||
props,
|
||||
data() {
|
||||
return {
|
||||
canvasWidth: null,
|
||||
canvasHeight: null,
|
||||
offscreenWidth: null,
|
||||
offscreenHeight: null,
|
||||
useCanvas2d: true,
|
||||
show: true,
|
||||
offscreenStyles: '',
|
||||
showMask: false,
|
||||
showOffscreen: false,
|
||||
isPC: false,
|
||||
// #ifdef APP-PLUS
|
||||
rclear: 0,
|
||||
rdestroy: 0,
|
||||
rundo: 0,
|
||||
rredo: 0,
|
||||
rsave: JSON.stringify({
|
||||
n: 0,
|
||||
fileType: 'png',
|
||||
quality: 1,
|
||||
destWidth: 0,
|
||||
destHeight: 0,
|
||||
}),
|
||||
rmask: JSON.stringify({
|
||||
n: 0,
|
||||
destWidth: 0,
|
||||
destHeight: 0,
|
||||
}),
|
||||
rempty: 0,
|
||||
risEmpty: true,
|
||||
toDataURL: null,
|
||||
tempFilePath: [],
|
||||
// #endif
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
canvasId() {
|
||||
// #ifdef VUE2
|
||||
return `lime-signature${this._uid}`
|
||||
// #endif
|
||||
// #ifdef VUE3
|
||||
return `lime-signature${this._.uid}`
|
||||
// #endif
|
||||
},
|
||||
offscreenId() {
|
||||
return this.canvasId + 'offscreen'
|
||||
},
|
||||
offscreenSize() {
|
||||
const {
|
||||
offscreenWidth,
|
||||
offscreenHeight
|
||||
} = this
|
||||
return this.landscape ? [offscreenHeight, offscreenWidth] : [offscreenWidth, offscreenHeight]
|
||||
},
|
||||
canvasStyle() {
|
||||
const {
|
||||
canvasWidth,
|
||||
canvasHeight,
|
||||
backgroundColor
|
||||
} = this
|
||||
return {
|
||||
width: canvasWidth && (canvasWidth + 'px'),
|
||||
height: canvasHeight && (canvasHeight + 'px'),
|
||||
background: backgroundColor
|
||||
}
|
||||
},
|
||||
param() {
|
||||
const {
|
||||
penColor,
|
||||
penSize,
|
||||
backgroundColor,
|
||||
backgroundImage,
|
||||
landscape,
|
||||
boundingBox,
|
||||
openSmooth,
|
||||
minLineWidth,
|
||||
maxLineWidth,
|
||||
minSpeed,
|
||||
maxWidthDiffRate,
|
||||
maxHistoryLength,
|
||||
disableScroll,
|
||||
disabled
|
||||
} = this
|
||||
return JSON.parse(JSON.stringify({
|
||||
penColor,
|
||||
penSize,
|
||||
backgroundColor,
|
||||
backgroundImage,
|
||||
landscape,
|
||||
boundingBox,
|
||||
openSmooth,
|
||||
minLineWidth,
|
||||
maxLineWidth,
|
||||
minSpeed,
|
||||
maxWidthDiffRate,
|
||||
maxHistoryLength,
|
||||
disableScroll,
|
||||
disabled
|
||||
}))
|
||||
}
|
||||
},
|
||||
// #ifdef APP-NVUE
|
||||
watch: {
|
||||
param(v) {
|
||||
this.$refs.webview.evalJS(`update(${JSON.stringify(v)})`)
|
||||
}
|
||||
},
|
||||
// #endif
|
||||
// #ifndef APP-PLUS
|
||||
created() {
|
||||
const {
|
||||
platform
|
||||
} = uni.getSystemInfoSync()
|
||||
this.isPC = /windows|mac/.test(platform)
|
||||
this.useCanvas2d = this.type == '2d' && canIUseCanvas2d() && !this.isPC
|
||||
// #ifndef H5
|
||||
this.showMask = this.isPC
|
||||
// #endif
|
||||
|
||||
},
|
||||
// #endif
|
||||
// #ifndef APP-PLUS
|
||||
async mounted() {
|
||||
if (this.beforeDelay) {
|
||||
await sleep(this.beforeDelay)
|
||||
}
|
||||
const config = await this.getContext()
|
||||
this.signature = new Signature(config)
|
||||
this.canvasEl = this.signature.canvas.get('el')
|
||||
this.offscreenWidth = this.canvasWidth = this.signature.canvas.get('width')
|
||||
this.offscreenHeight = this.canvasHeight = this.signature.canvas.get('height')
|
||||
|
||||
this.stopWatch = this.$watch('param', (v) => {
|
||||
this.signature.pen.setOption(v)
|
||||
}, {
|
||||
immediate: true
|
||||
})
|
||||
},
|
||||
// #endif
|
||||
// #ifndef APP-PLUS
|
||||
// #ifdef VUE3
|
||||
beforeUnmount() {
|
||||
this.stopWatch && this.stopWatch()
|
||||
this.signature.destroy()
|
||||
this.signature = null
|
||||
this.show = false;
|
||||
// #ifdef APP-VUE || APP-NVUE
|
||||
this.rdestroy++
|
||||
// #endif
|
||||
},
|
||||
// #endif
|
||||
// #ifdef VUE2
|
||||
beforeDestroy() {
|
||||
this.stopWatch && this.stopWatch()
|
||||
this.signature.destroy()
|
||||
this.show = false;
|
||||
this.signature = null
|
||||
// #ifdef APP-VUE || APP-NVUE
|
||||
this.rdestroy++
|
||||
// #endif
|
||||
},
|
||||
// #endif
|
||||
// #endif
|
||||
methods: {
|
||||
// #ifdef MP-QQ
|
||||
// toJSON() { return this },
|
||||
// #endif
|
||||
// #ifdef APP-PLUS
|
||||
onPageFinish() {
|
||||
this.$refs.webview.evalJS(`update(${JSON.stringify(this.param)})`)
|
||||
},
|
||||
onMessage(e = {}) {
|
||||
const {
|
||||
detail: {
|
||||
data: [res]
|
||||
}
|
||||
} = e
|
||||
if (res.event?.save) {
|
||||
this.toDataURL = res.event.save
|
||||
}
|
||||
if (res.event?.changeSize) {
|
||||
const {
|
||||
width,
|
||||
height
|
||||
} = res.event.changeSize
|
||||
}
|
||||
if (res.event.hasOwnProperty('isEmpty')) {
|
||||
this.risEmpty = res.event.isEmpty
|
||||
}
|
||||
if (res.event?.file) {
|
||||
this.tempFilePath.push(res.event.file)
|
||||
if (this.tempFilePath.length > 7) {
|
||||
this.tempFilePath.shift()
|
||||
}
|
||||
return
|
||||
}
|
||||
if (res.event?.success) {
|
||||
if (res.event.success) {
|
||||
this.tempFilePath.push(res.event.success)
|
||||
if (this.tempFilePath.length > 8) {
|
||||
this.tempFilePath.shift()
|
||||
}
|
||||
this.toDataURL = this.tempFilePath.join('')
|
||||
this.tempFilePath = []
|
||||
} else {
|
||||
this.$emit('fail', 'canvas no data')
|
||||
}
|
||||
return
|
||||
}
|
||||
},
|
||||
// #endif
|
||||
redo() {
|
||||
// #ifdef APP-VUE || APP-NVUE
|
||||
this.rredo += 1
|
||||
// #endif
|
||||
// #ifdef APP-NVUE
|
||||
this.$refs.webview.evalJS(`redo()`)
|
||||
// #endif
|
||||
// #ifndef APP-VUE
|
||||
if (this.signature)
|
||||
this.signature.redo()
|
||||
// #endif
|
||||
},
|
||||
restore() {
|
||||
this.redo()
|
||||
},
|
||||
undo() {
|
||||
// #ifdef APP-VUE || APP-NVUE
|
||||
this.rundo += 1
|
||||
// #endif
|
||||
// #ifdef APP-NVUE
|
||||
this.$refs.webview.evalJS(`undo()`)
|
||||
// #endif
|
||||
// #ifndef APP-VUE
|
||||
if (this.signature)
|
||||
this.signature.undo()
|
||||
// #endif
|
||||
},
|
||||
clear() {
|
||||
// #ifdef APP-VUE || APP-NVUE
|
||||
this.rclear += 1
|
||||
// #endif
|
||||
// #ifdef APP-NVUE
|
||||
this.$refs.webview.evalJS(`clear()`)
|
||||
// #endif
|
||||
// #ifndef APP-VUE
|
||||
if (this.signature)
|
||||
this.signature.clear()
|
||||
// #endif
|
||||
},
|
||||
isEmpty() {
|
||||
// #ifdef APP-NVUE
|
||||
this.$refs.webview.evalJS(`isEmpty()`)
|
||||
// #endif
|
||||
// #ifdef APP-VUE || APP-NVUE
|
||||
this.rempty += 1
|
||||
// #endif
|
||||
// #ifndef APP-VUE || APP-NVUE
|
||||
return this.signature.isEmpty()
|
||||
// #endif
|
||||
},
|
||||
async canvasToMaskPath(param = {}) {
|
||||
const isEmpty = this.isEmpty()
|
||||
// #ifdef APP-NVUE
|
||||
this.$refs.webview.evalJS(`mask(${JSON.stringify(param)})`)
|
||||
// #endif
|
||||
// #ifdef APP-VUE || APP-NVUE
|
||||
const stopURLWatch = this.$watch('toDataURL', (v, n) => {
|
||||
if (v && v !== n) {
|
||||
// if(param.pathType == 'url') {
|
||||
base64ToPath(v).then(res => {
|
||||
param.success({
|
||||
tempFilePath: res,
|
||||
isEmpty: this.risEmpty
|
||||
})
|
||||
})
|
||||
// } else {
|
||||
// param.success({tempFilePath: v,isEmpty: this.risEmpty })
|
||||
// }
|
||||
this.toDataURL = ''
|
||||
}
|
||||
stopURLWatch && stopURLWatch()
|
||||
})
|
||||
const {
|
||||
fileType,
|
||||
quality
|
||||
} = param
|
||||
const rmask = JSON.parse(this.rmask)
|
||||
rmask.n++
|
||||
rmask.destWidth = param.destWidth ?? 0
|
||||
rmask.destHeight = param.destHeight ?? 0
|
||||
// rmask.fileType = fileType
|
||||
// rmask.quality = quality
|
||||
this.rmask = JSON.stringify(rmask)
|
||||
// #endif
|
||||
// #ifndef APP-VUE || APP-NVUE
|
||||
this.showOffscreen = true
|
||||
|
||||
let width = this.signature.canvas.get('width')
|
||||
let height = this.signature.canvas.get('height')
|
||||
let {
|
||||
pixelRatio
|
||||
} = uni.getSystemInfoSync()
|
||||
if (this.useCanvas2d) {
|
||||
this.offscreenWidth = width * pixelRatio
|
||||
this.offscreenHeight = height * pixelRatio
|
||||
} else {
|
||||
this.offscreenWidth = width
|
||||
this.offscreenHeight = height
|
||||
}
|
||||
await sleep(100)
|
||||
const context = uni.createCanvasContext('offscreen', this)
|
||||
const size = Math.max(this.offscreenWidth, this.offscreenHeight)
|
||||
const success = (success) => param.success && param.success(success)
|
||||
const fail = (fail) => param.fail && param.fail(fail)
|
||||
|
||||
this.signature.pen.getMaskedImageData((imageData) => {
|
||||
let canvasPutImageData = (options, comp) => {
|
||||
if (uni.canvasPutImageData) {
|
||||
uni.canvasPutImageData(options, comp)
|
||||
} else if (context.putImageData) {
|
||||
context.putImageData(options)
|
||||
}
|
||||
}
|
||||
canvasPutImageData({
|
||||
canvasId: 'offscreen',
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: width,
|
||||
height:height,
|
||||
data: imageData,
|
||||
fail(err) {
|
||||
fail(err)
|
||||
},
|
||||
success: (re) => {
|
||||
toDataURL('offscreen', this, param).then((res) => {
|
||||
context.restore()
|
||||
context.clearRect(0, 0, size, size)
|
||||
this.offscreenWidth = width
|
||||
this.offscreenHeight = height
|
||||
this.showOffscreen = false
|
||||
success({
|
||||
tempFilePath: res,
|
||||
isEmpty
|
||||
})
|
||||
})
|
||||
}
|
||||
}, this)
|
||||
|
||||
})
|
||||
// #endif
|
||||
},
|
||||
canvasToTempFilePath(param = {}) {
|
||||
|
||||
const isEmpty = this.isEmpty()
|
||||
// #ifdef APP-NVUE
|
||||
this.$refs.webview.evalJS(`save(${JSON.stringify(param)})`)
|
||||
// #endif
|
||||
// #ifdef APP-VUE || APP-NVUE
|
||||
const stopURLWatch = this.$watch('toDataURL', (v, n) => {
|
||||
if (v && v !== n) {
|
||||
if (this.preferToDataURL) {
|
||||
param.success({
|
||||
tempFilePath: v,
|
||||
isEmpty: this.risEmpty
|
||||
})
|
||||
} else {
|
||||
base64ToPath(v).then(res => {
|
||||
param.success({
|
||||
tempFilePath: res,
|
||||
isEmpty: this.risEmpty
|
||||
})
|
||||
})
|
||||
}
|
||||
this.toDataURL = ''
|
||||
}
|
||||
stopURLWatch && stopURLWatch()
|
||||
})
|
||||
const {
|
||||
fileType,
|
||||
quality
|
||||
} = param
|
||||
const rsave = JSON.parse(this.rsave)
|
||||
rsave.n++
|
||||
rsave.fileType = fileType
|
||||
rsave.quality = quality
|
||||
rsave.destWidth = param.destWidth ?? 0
|
||||
rsave.destHeight = param.destHeight ?? 0
|
||||
this.rsave = JSON.stringify(rsave)
|
||||
// #endif
|
||||
// #ifndef APP-VUE || APP-NVUE
|
||||
const useCanvas2d = this.useCanvas2d
|
||||
const success = (success) => param.success && param.success(success)
|
||||
const fail = (err) => param.fail && param.fail(err)
|
||||
const {
|
||||
canvas
|
||||
} = this.signature.canvas.get('el')
|
||||
const {
|
||||
backgroundColor,
|
||||
landscape,
|
||||
boundingBox
|
||||
} = this
|
||||
let width = this.signature.canvas.get('width')
|
||||
let height = this.signature.canvas.get('height')
|
||||
let x = 0
|
||||
let y = 0
|
||||
const devtools = uni.getSystemInfoSync().platform == 'devtools'
|
||||
let preferToDataURL = this.preferToDataURL
|
||||
let scale = 1
|
||||
// #ifdef MP-TOUTIAO
|
||||
scale = devtools ? uni.getSystemInfoSync().pixelRatio : scale
|
||||
// 由于抖音不支持canvasToTempFilePath故优先使用createOffscreenCanvas
|
||||
preferToDataURL = true
|
||||
// #endif
|
||||
const canvasToTempFilePath = async (image) => {
|
||||
const createCanvasContext = () => {
|
||||
const useOffscreen = (useCanvas2d && !!uni.createOffscreenCanvas && preferToDataURL)
|
||||
if (useOffscreen && !devtools) {
|
||||
const offCanvas = uni.createOffscreenCanvas({
|
||||
type: '2d'
|
||||
});
|
||||
offCanvas.width = this.offscreenSize[0] * scale
|
||||
offCanvas.height = this.offscreenSize[1] * scale
|
||||
const context = offCanvas.getContext("2d");
|
||||
return [context, offCanvas]
|
||||
} else {
|
||||
const context = uni.createCanvasContext('offscreen', this)
|
||||
return [context]
|
||||
}
|
||||
}
|
||||
|
||||
if (boundingBox && !this.isPC || landscape || backgroundColor && !isTransparent(backgroundColor)) {
|
||||
|
||||
this.showOffscreen = true
|
||||
await sleep(100)
|
||||
const [context, offCanvas] = createCanvasContext()
|
||||
context.save()
|
||||
context.setTransform(1, 0, 0, 1, 0, 0)
|
||||
if (landscape) {
|
||||
context.translate(0, width * scale)
|
||||
context.rotate(-Math.PI / 2)
|
||||
}
|
||||
if (backgroundColor && !isTransparent(backgroundColor)) {
|
||||
context.fillStyle = backgroundColor
|
||||
context.fillRect(0, 0, width, height)
|
||||
}
|
||||
if (offCanvas) {
|
||||
const img = canvas.createImage();
|
||||
img.src = image
|
||||
img.onload = () => {
|
||||
context.drawImage(img, 0, 0, width * scale, height * scale);
|
||||
const tempFilePath = offCanvas.toDataURL()
|
||||
this.showOffscreen = false
|
||||
success({
|
||||
tempFilePath,
|
||||
isEmpty
|
||||
})
|
||||
}
|
||||
|
||||
} else {
|
||||
context.drawImage(image, 0, 0, width * scale, height * scale);
|
||||
context.draw(false, () => {
|
||||
toDataURL('offscreen', this, param).then((res) => {
|
||||
const size = Math.max(width, height)
|
||||
context.restore()
|
||||
context.clearRect(0, 0, size, size)
|
||||
this.showOffscreen = false
|
||||
success({
|
||||
tempFilePath: res,
|
||||
isEmpty
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
} else {
|
||||
success({
|
||||
tempFilePath: image,
|
||||
isEmpty
|
||||
})
|
||||
}
|
||||
}
|
||||
const next = async () => {
|
||||
if (this.offscreenWidth != width || this.offscreenHeight != height) {
|
||||
this.offscreenWidth = width
|
||||
this.offscreenHeight = height
|
||||
await sleep(100)
|
||||
}
|
||||
|
||||
// #ifndef MP-WEIXIN
|
||||
const param = {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
canvas,
|
||||
preferToDataURL
|
||||
}
|
||||
// #endif
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
const param = {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
canvas: useCanvas2d ? canvas : null,
|
||||
preferToDataURL
|
||||
}
|
||||
// #endif
|
||||
toDataURL(this.canvasId, this, param).then(canvasToTempFilePath).catch(fail)
|
||||
}
|
||||
// PC端小程序获取不到 ImageData 数据,长度为0
|
||||
if (boundingBox && !this.isPC) {
|
||||
this.signature.getContentBoundingBox(async res => {
|
||||
this.offscreenWidth = width = res.width
|
||||
this.offscreenHeight = height = res.height
|
||||
|
||||
x = res.startX
|
||||
y = res.startY
|
||||
|
||||
next()
|
||||
})
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
// #endif
|
||||
},
|
||||
// #ifndef APP-PLUS
|
||||
getContext() {
|
||||
return getRect(`#${this.canvasId}`, {
|
||||
context: this,
|
||||
type: this.useCanvas2d ? 'fields' : 'boundingClientRect'
|
||||
}).then(res => {
|
||||
if (res) {
|
||||
let {
|
||||
width,
|
||||
height,
|
||||
node: canvas,
|
||||
left,
|
||||
top,
|
||||
right
|
||||
} = res
|
||||
let {
|
||||
pixelRatio
|
||||
} = uni.getSystemInfoSync()
|
||||
let context;
|
||||
if (canvas) {
|
||||
context = canvas.getContext('2d')
|
||||
canvas.width = width * pixelRatio;
|
||||
canvas.height = height * pixelRatio;
|
||||
} else {
|
||||
pixelRatio = 1
|
||||
context = uniContext(this.canvasId, this)
|
||||
canvas = {
|
||||
getContext: (type) => type == '2d' ? context : null,
|
||||
createImage,
|
||||
toDataURL: () => toDataURL(this.canvasId, this),
|
||||
requestAnimationFrame
|
||||
}
|
||||
}
|
||||
// 支付宝小程序 使用stroke有个默认背景色
|
||||
context.clearRect(0, 0, width, height)
|
||||
return {
|
||||
left,
|
||||
top,
|
||||
right,
|
||||
width,
|
||||
height,
|
||||
context,
|
||||
canvas,
|
||||
pixelRatio
|
||||
};
|
||||
}
|
||||
})
|
||||
},
|
||||
getTouch(e) {
|
||||
if (this.isPC && this.canvasRect) {
|
||||
e.touches = e.touches.map(item => {
|
||||
return {
|
||||
...item,
|
||||
x: item.clientX - this.canvasRect.left,
|
||||
y: item.clientY - this.canvasRect.top,
|
||||
}
|
||||
})
|
||||
}
|
||||
return e
|
||||
},
|
||||
touchStart(e) {
|
||||
if (!this.canvasEl) return
|
||||
this.isStart = true
|
||||
// 微信小程序PC端不支持事件,使用这方法模拟一下
|
||||
if (this.isPC) {
|
||||
getRect(`#${this.canvasId}`, {
|
||||
context: this
|
||||
}).then(res => {
|
||||
this.canvasRect = res
|
||||
this.canvasEl.dispatchEvent('touchstart', wrapEvent(this.getTouch(e)))
|
||||
})
|
||||
return
|
||||
}
|
||||
this.canvasEl.dispatchEvent('touchstart', wrapEvent(e))
|
||||
},
|
||||
touchMove(e) {
|
||||
if (!this.canvasEl || !this.isStart && this.canvasEl) return
|
||||
this.canvasEl.dispatchEvent('touchmove', wrapEvent(this.getTouch(e)))
|
||||
},
|
||||
touchEnd(e) {
|
||||
if (!this.canvasEl) return
|
||||
this.isStart = false
|
||||
this.canvasEl.dispatchEvent('touchend', wrapEvent(e))
|
||||
},
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<!-- #ifdef APP-VUE -->
|
||||
<script module="sign" lang="renderjs">
|
||||
import sign from './render'
|
||||
export default sign
|
||||
</script>
|
||||
<!-- #endif -->
|
||||
<style lang="scss">
|
||||
.lime-signature,
|
||||
.lime-signature__canvas {
|
||||
/* #ifndef APP-NVUE */
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
/* #endif */
|
||||
/* #ifdef APP-NVUE */
|
||||
flex: 1;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.mask {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.offscreen {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
// left: 0;
|
||||
pointer-events:none;
|
||||
// background: rgba(0,255,0,0.5);
|
||||
left: 9999px;
|
||||
}
|
||||
</style>
|
||||
64
uni_modules/jp-signature/components/jp-signature/props.js
Normal file
64
uni_modules/jp-signature/components/jp-signature/props.js
Normal file
@@ -0,0 +1,64 @@
|
||||
export default {
|
||||
styles: String,
|
||||
disableScroll: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: '2d'
|
||||
},
|
||||
// 画笔颜色
|
||||
penColor: {
|
||||
type: String,
|
||||
default: 'black'
|
||||
},
|
||||
penSize: {
|
||||
type: Number,
|
||||
default: 2
|
||||
},
|
||||
// 画板背景颜色
|
||||
backgroundColor: String,
|
||||
backgroundImage: String,
|
||||
// 笔锋
|
||||
openSmooth: Boolean,
|
||||
// 画笔最小值
|
||||
minLineWidth: {
|
||||
type: Number,
|
||||
default: 2
|
||||
},
|
||||
// 画笔最大值
|
||||
maxLineWidth: {
|
||||
type: Number,
|
||||
default: 6
|
||||
},
|
||||
// 画笔达到最小宽度所需最小速度(px/ms),取值范围1.0-10.0,值越小,画笔越容易变细,笔锋效果会比较明显,可以自行调整查看效果,选出自己满意的值。
|
||||
minSpeed: {
|
||||
type: Number,
|
||||
default: 1.5
|
||||
},
|
||||
// 相邻两线宽度增(减)量最大百分比,取值范围1-100,为了达到笔锋效果,画笔宽度会随画笔速度而改变,如果相邻两线宽度差太大,过渡效果就会很突兀,使用maxWidthDiffRate限制宽度差,让过渡效果更自然。可以自行调整查看效果,选出自己满意的值。
|
||||
maxWidthDiffRate: {
|
||||
type: Number,
|
||||
default: 20
|
||||
},
|
||||
// 限制历史记录数,即最大可撤销数,传入0则关闭历史记录功能
|
||||
maxHistoryLength: {
|
||||
type: Number,
|
||||
default: 20
|
||||
},
|
||||
beforeDelay: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
landscape: {
|
||||
type: Boolean
|
||||
},
|
||||
boundingBox: {
|
||||
type: Boolean
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean
|
||||
},
|
||||
preferToDataURL: Boolean
|
||||
}
|
||||
228
uni_modules/jp-signature/components/jp-signature/render.js
Normal file
228
uni_modules/jp-signature/components/jp-signature/render.js
Normal file
@@ -0,0 +1,228 @@
|
||||
// #ifdef APP-VUE
|
||||
// import { Signature } from '@signature'
|
||||
import {
|
||||
Signature
|
||||
} from './signature.js'
|
||||
import {
|
||||
isTransparent
|
||||
} from './utils'
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
canvasid: null,
|
||||
signature: null,
|
||||
observer: null,
|
||||
options: {},
|
||||
saveCount: 0,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(this.init)
|
||||
},
|
||||
methods: {
|
||||
init() {
|
||||
const el = this.$refs.limeSignature || this.$ownerInstance.$el;
|
||||
this.canvas = document.createElement('canvas')
|
||||
this.canvas.style = 'width: 100%; height: 100%;'
|
||||
el.appendChild(this.canvas)
|
||||
this.signature = new Signature({
|
||||
el: this.canvas
|
||||
})
|
||||
this.signature.pen.setOption(this.options)
|
||||
const width = this.signature.canvas.get('width')
|
||||
const height = this.signature.canvas.get('height')
|
||||
|
||||
this.emit({
|
||||
changeSize: {
|
||||
width,
|
||||
height
|
||||
}
|
||||
})
|
||||
},
|
||||
redo(v) {
|
||||
if (v && this.signature) {
|
||||
this.signature.redo()
|
||||
}
|
||||
},
|
||||
undo(v) {
|
||||
if (v && this.signature) {
|
||||
this.signature.undo()
|
||||
}
|
||||
},
|
||||
clear(v) {
|
||||
if (v && this.signature) {
|
||||
this.signature.clear()
|
||||
}
|
||||
},
|
||||
destroy() {
|
||||
if (this.canvas) {
|
||||
this.canvas.remove()
|
||||
}
|
||||
},
|
||||
mask(param={}) {
|
||||
if (this.signature) {
|
||||
let {destWidth=0, destHeight=0} = JSON.parse(param)
|
||||
let canvas = document.createElement('canvas')
|
||||
const ctx = canvas.getContext('2d');
|
||||
const pixelRatio = this.signature.canvas.get('pixelRatio')
|
||||
let width = this.signature.canvas.get('width')
|
||||
let height = this.signature.canvas.get('height')
|
||||
let context = this.signature.canvas.get('context')
|
||||
canvas.width = width * pixelRatio
|
||||
canvas.height = height * pixelRatio
|
||||
|
||||
const imageData = context.getImageData(0, 0, width * pixelRatio, height * pixelRatio);
|
||||
for (let i = 0; i < imageData.data.length; i += 4) {
|
||||
// 判断当前像素是否透明
|
||||
const isTransparent = imageData.data[i + 3] === 0;
|
||||
|
||||
if (isTransparent) {
|
||||
// 将透明像素设置为黑色背景
|
||||
imageData.data[i] = 0;
|
||||
imageData.data[i + 1] = 0;
|
||||
imageData.data[i + 2] = 0;
|
||||
} else {
|
||||
// 将非透明像素设置为白色内容
|
||||
imageData.data[i] = 255;
|
||||
imageData.data[i + 1] = 255;
|
||||
imageData.data[i + 2] = 255;
|
||||
}
|
||||
}
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
if(destWidth&&destHeight){
|
||||
const _canvas = document.createElement('canvas')
|
||||
_canvas.width = destWidth
|
||||
_canvas.height = destHeight
|
||||
const _context = _canvas.getContext('2d')
|
||||
_context.drawImage(canvas, 0, 0, destWidth, destHeight)
|
||||
canvas.remove()
|
||||
canvas = _canvas
|
||||
}
|
||||
this.emit({
|
||||
save: canvas.toDataURL()
|
||||
})
|
||||
canvas.remove()
|
||||
}
|
||||
|
||||
},
|
||||
save(param) {
|
||||
let {
|
||||
fileType = 'png',
|
||||
quality = 1,
|
||||
n,
|
||||
destWidth = 0,
|
||||
destHeight = 0,
|
||||
} = JSON.parse(param)
|
||||
const type = `image/${fileType}`.replace(/jpg/, 'jpeg');
|
||||
if (n !== this.saveCount) {
|
||||
this.saveCount = n;
|
||||
const {
|
||||
backgroundColor,
|
||||
backgroundImage,
|
||||
landscape,
|
||||
boundingBox
|
||||
} = this.options
|
||||
const flag = landscape || backgroundColor || boundingBox||destWidth&&destHeight
|
||||
const image = this.signature.canvas.get('el').toDataURL(!flag && type, !flag && quality)
|
||||
if (flag) {
|
||||
let canvas = document.createElement('canvas')
|
||||
const pixelRatio = this.signature.canvas.get('pixelRatio')
|
||||
let width = this.signature.canvas.get('width')
|
||||
let height = this.signature.canvas.get('height')
|
||||
let x = 0
|
||||
let y = 0
|
||||
|
||||
const next = () => {
|
||||
const size = [width, height]
|
||||
if (landscape) {
|
||||
size.reverse()
|
||||
}
|
||||
canvas.width = size[0] * pixelRatio
|
||||
canvas.height = size[1] * pixelRatio
|
||||
const param = [x, y, width, height, 0, 0, width, height].map(item => item * pixelRatio)
|
||||
const context = canvas.getContext('2d')
|
||||
if (landscape) {
|
||||
context.translate(0, width * pixelRatio)
|
||||
context.rotate(-Math.PI / 2)
|
||||
}
|
||||
if (backgroundColor && !isTransparent(backgroundColor)) {
|
||||
context.fillStyle = backgroundColor
|
||||
context.fillRect(0, 0, width * pixelRatio, height * pixelRatio)
|
||||
}
|
||||
const drawImage = () => {
|
||||
// param
|
||||
context.drawImage(this.signature.canvas.get('el'), ...param)
|
||||
if(destWidth&&destHeight){
|
||||
const _canvas = document.createElement('canvas')
|
||||
_canvas.width = destWidth
|
||||
_canvas.height = destHeight
|
||||
const _context = _canvas.getContext('2d')
|
||||
_context.drawImage(canvas, 0, 0, destWidth, destHeight)
|
||||
canvas.remove()
|
||||
canvas = _canvas
|
||||
}
|
||||
this.emit({
|
||||
save: canvas.toDataURL(type, quality)
|
||||
})
|
||||
canvas.remove()
|
||||
}
|
||||
if (backgroundImage) {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
context.drawImage(img, ...param)
|
||||
drawImage()
|
||||
}
|
||||
img.src = backgroundImage
|
||||
}
|
||||
if (!backgroundImage) {
|
||||
drawImage()
|
||||
}
|
||||
}
|
||||
if (boundingBox) {
|
||||
const res = this.signature.getContentBoundingBox()
|
||||
width = res.width
|
||||
height = res.height
|
||||
x = res.startX
|
||||
y = res.startY
|
||||
next()
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
|
||||
} else {
|
||||
this.emit({
|
||||
save: image
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
isEmpty(v) {
|
||||
if (v && this.signature) {
|
||||
const isEmpty = this.signature.isEmpty()
|
||||
this.emit({
|
||||
isEmpty
|
||||
})
|
||||
}
|
||||
},
|
||||
emit(event) {
|
||||
this.$ownerInstance.callMethod('onMessage', {
|
||||
detail: {
|
||||
data: [{
|
||||
event
|
||||
}]
|
||||
}
|
||||
})
|
||||
},
|
||||
update(v) {
|
||||
if (v) {
|
||||
if (this.signature) {
|
||||
this.options = v
|
||||
this.signature.pen.setOption(v)
|
||||
} else {
|
||||
this.options = v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
File diff suppressed because one or more lines are too long
165
uni_modules/jp-signature/components/jp-signature/signature.uts
Normal file
165
uni_modules/jp-signature/components/jp-signature/signature.uts
Normal file
@@ -0,0 +1,165 @@
|
||||
import { LSignatureOptions, Point, Line } from '../../index.uts'
|
||||
|
||||
let points : Line = []
|
||||
let undoStack : Line[] = [];
|
||||
let redoStack : Line[] = [];
|
||||
let lastX = 0;
|
||||
let lastY = 0;
|
||||
|
||||
|
||||
export class Signature {
|
||||
el : UniElement
|
||||
options : LSignatureOptions = {
|
||||
penColor: 'black',
|
||||
openSmooth: true,
|
||||
disableScroll: true,
|
||||
disabled: false,
|
||||
penSize: 2,
|
||||
minLineWidth: 2,
|
||||
maxLineWidth: 6,
|
||||
minSpeed: 1.5,
|
||||
maxWidthDiffRate: 20,
|
||||
maxHistoryLength: 20
|
||||
} as LSignatureOptions
|
||||
ctx : DrawableContext
|
||||
isEmpty : boolean = true
|
||||
isDrawing : boolean = false
|
||||
// historyList : Point[][] = []
|
||||
// id : string
|
||||
// instance : ComponentPublicInstance
|
||||
touchstartCallbackWrapper: UniCallbackWrapper|null = null
|
||||
touchmoveCallbackWrapper: UniCallbackWrapper|null= null
|
||||
touchendCallbackWrapper: UniCallbackWrapper|null= null
|
||||
constructor(el : UniElement) {
|
||||
this.el = el
|
||||
this.ctx = el.getDrawableContext() as DrawableContext
|
||||
this.init()
|
||||
}
|
||||
init() {
|
||||
this.touchstartCallbackWrapper = this.el.addEventListener('touchstart', this.onTouchStart)
|
||||
this.touchmoveCallbackWrapper = this.el.addEventListener('touchmove', this.onTouchMove)
|
||||
this.touchendCallbackWrapper = this.el.addEventListener('touchend', this.onTouchEnd)
|
||||
}
|
||||
remove() {
|
||||
if(this.touchstartCallbackWrapper == null) return
|
||||
this.el.removeEventListener('touchstart', this.touchstartCallbackWrapper!)
|
||||
this.el.removeEventListener('touchmove', this.touchmoveCallbackWrapper!)
|
||||
this.el.removeEventListener('touchend', this.touchendCallbackWrapper!)
|
||||
}
|
||||
setOption(options : LSignatureOptions) {
|
||||
this.options = options
|
||||
}
|
||||
disableScroll(event : UniTouchEvent) {
|
||||
event.stopPropagation()
|
||||
if (this.options.disableScroll) {
|
||||
{
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
}
|
||||
getTouchPoint(event : UniTouchEvent) : Point {
|
||||
const rect = this.el.getBoundingClientRect()
|
||||
const touche = event.touches[0];
|
||||
const x = touche.clientX
|
||||
const y = touche.clientY
|
||||
// const force = touche.force
|
||||
return {
|
||||
x: x - rect.left,
|
||||
y: y - rect.top
|
||||
} as Point
|
||||
}
|
||||
onTouchStart: (event : UniTouchEvent) => void = (event : UniTouchEvent) =>{
|
||||
if (this.options.disabled) {
|
||||
return
|
||||
}
|
||||
this.disableScroll(event)
|
||||
const { x, y } = this.getTouchPoint(event)
|
||||
this.isDrawing = true;
|
||||
this.isEmpty = false
|
||||
lastX = x
|
||||
lastY = y
|
||||
points.push({ x, y } as Point);
|
||||
}
|
||||
onTouchMove: (event : UniTouchEvent) => void = (event : UniTouchEvent) =>{
|
||||
if (this.options.disabled || !this.isDrawing) {
|
||||
return
|
||||
}
|
||||
this.disableScroll(event)
|
||||
const { x, y } = this.getTouchPoint(event)
|
||||
const lineWidth = this.options.penSize
|
||||
const strokeStyle = this.options.penColor
|
||||
const point = { x, y } as Point
|
||||
const last = { x: lastX, y: lastY } as Point
|
||||
this.drawLine(point, last, lineWidth, strokeStyle)
|
||||
|
||||
lastX = x
|
||||
lastY = y
|
||||
points.push({ x, y, c: strokeStyle, w: lineWidth } as Point);
|
||||
}
|
||||
onTouchEnd: (event : UniTouchEvent) => void = (event : UniTouchEvent) =>{
|
||||
this.disableScroll(event)
|
||||
this.isDrawing = false;
|
||||
undoStack.push(points);
|
||||
redoStack = [] as Line[];
|
||||
points = [] as Point[];
|
||||
}
|
||||
drawLine(point : Point, last : Point, lineWidth : number, strokeStyle : string) {
|
||||
const ctx = this.ctx
|
||||
ctx.lineWidth = lineWidth
|
||||
ctx.strokeStyle = strokeStyle
|
||||
ctx.lineCap = 'round'
|
||||
ctx.lineJoin = 'round'
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(last.x, last.y)
|
||||
ctx.lineTo(point.x, point.y)
|
||||
ctx.stroke()
|
||||
ctx.update()
|
||||
}
|
||||
// addHistory() { }
|
||||
clear() {
|
||||
this.ctx.reset()
|
||||
this.ctx.update()
|
||||
this.isEmpty = true
|
||||
undoStack = [] as Line[];
|
||||
redoStack = [] as Line[];
|
||||
points = [] as Point[];
|
||||
}
|
||||
undo() {
|
||||
if(redoStack.length == this.options.maxHistoryLength && this.options.maxHistoryLength != 0){
|
||||
return
|
||||
}
|
||||
this.ctx.reset()
|
||||
if(undoStack.length > 0){
|
||||
const lastPath : Line = undoStack.pop()!;
|
||||
redoStack.push(lastPath);
|
||||
if(undoStack.length == 0){
|
||||
this.isEmpty = true
|
||||
this.ctx.update()
|
||||
return
|
||||
}
|
||||
for (let l = 0; l < undoStack.length; l++) {
|
||||
for (let i = 1; i < undoStack[l].length; i++) {
|
||||
const last = undoStack[l][i - 1]
|
||||
const point = undoStack[l][i]
|
||||
this.drawLine(point, last, point.w!, point.c!)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.ctx.update()
|
||||
}
|
||||
}
|
||||
redo() {
|
||||
if(redoStack.length < 1) return
|
||||
const lastPath : Line = redoStack.pop()!;
|
||||
undoStack.push(lastPath);
|
||||
this.isEmpty = false
|
||||
for (let l = 0; l < undoStack.length; l++) {
|
||||
for (let i = 1; i < undoStack[l].length; i++) {
|
||||
const last = undoStack[l][i - 1]
|
||||
const point = undoStack[l][i]
|
||||
this.drawLine(point, last, point.w!, point.c!)
|
||||
}
|
||||
}
|
||||
}
|
||||
// restore() { }
|
||||
}
|
||||
181
uni_modules/jp-signature/components/jp-signature/utils.js
Normal file
181
uni_modules/jp-signature/components/jp-signature/utils.js
Normal file
@@ -0,0 +1,181 @@
|
||||
export function compareVersion(v1, v2) {
|
||||
v1 = v1.split('.')
|
||||
v2 = v2.split('.')
|
||||
const len = Math.max(v1.length, v2.length)
|
||||
while (v1.length < len) {
|
||||
v1.push('0')
|
||||
}
|
||||
while (v2.length < len) {
|
||||
v2.push('0')
|
||||
}
|
||||
for (let i = 0; i < len; i++) {
|
||||
const num1 = parseInt(v1[i], 10)
|
||||
const num2 = parseInt(v2[i], 10)
|
||||
|
||||
if (num1 > num2) {
|
||||
return 1
|
||||
} else if (num1 < num2) {
|
||||
return -1
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
function gte(version) {
|
||||
let { SDKVersion } = uni.getSystemInfoSync()
|
||||
// #ifdef MP-ALIPAY
|
||||
SDKVersion = my.SDKVersion
|
||||
// #endif
|
||||
return compareVersion(SDKVersion, version) >= 0;
|
||||
}
|
||||
|
||||
export function canIUseCanvas2d() {
|
||||
// #ifdef MP-WEIXIN
|
||||
return gte('2.9.0');
|
||||
// #endif
|
||||
// #ifdef MP-ALIPAY
|
||||
return gte('2.7.0');
|
||||
// #endif
|
||||
// #ifdef MP-TOUTIAO
|
||||
return gte('1.78.0');
|
||||
// #endif
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
export const wrapEvent = (e) => {
|
||||
if (!e) return;
|
||||
if (!e.preventDefault) {
|
||||
e.preventDefault = function() {};
|
||||
}
|
||||
return e;
|
||||
}
|
||||
|
||||
export const requestAnimationFrame = (cb) => {
|
||||
setTimeout(cb, 30)
|
||||
}
|
||||
|
||||
// #ifdef MP
|
||||
export const prefix = () => {
|
||||
// #ifdef MP-TOUTIAO
|
||||
return tt
|
||||
// #endif
|
||||
// #ifdef MP-WEIXIN
|
||||
return wx
|
||||
// #endif
|
||||
// #ifdef MP-BAIDU
|
||||
return swan
|
||||
// #endif
|
||||
// #ifdef MP-ALIPAY
|
||||
return my
|
||||
// #endif
|
||||
// #ifdef MP-QQ
|
||||
return qq
|
||||
// #endif
|
||||
// #ifdef MP-360
|
||||
return qh
|
||||
// #endif
|
||||
}
|
||||
// #endif
|
||||
|
||||
/**
|
||||
* base64转路径
|
||||
* @param {Object} base64
|
||||
*/
|
||||
export function base64ToPath(base64) {
|
||||
const [, format, bodyData] = /data:image\/(\w+);base64,(.*)/.exec(base64) || [];
|
||||
return new Promise((resolve, reject) => {
|
||||
// #ifdef MP
|
||||
const p = prefix()
|
||||
const fs = p.getFileSystemManager()
|
||||
//自定义文件名
|
||||
if (!format) {
|
||||
reject(new Error('ERROR_BASE64SRC_PARSE'))
|
||||
}
|
||||
const time = new Date().getTime();
|
||||
const filePath = `${p.env.USER_DATA_PATH}/${time}.${format}`;
|
||||
fs.writeFile({
|
||||
filePath,
|
||||
data: base64.split(',')[1],
|
||||
encoding: 'base64',
|
||||
success() {
|
||||
resolve(filePath)
|
||||
},
|
||||
fail(err) {
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
// #ifdef APP-PLUS
|
||||
const bitmap = new plus.nativeObj.Bitmap('bitmap' + Date.now())
|
||||
bitmap.loadBase64Data(base64, () => {
|
||||
if (!format) {
|
||||
reject(new Error('ERROR_BASE64SRC_PARSE'))
|
||||
}
|
||||
const time = new Date().getTime();
|
||||
const filePath = `_doc/uniapp_temp/${time}.${format}`
|
||||
bitmap.save(filePath, {},
|
||||
() => {
|
||||
bitmap.clear()
|
||||
resolve(filePath)
|
||||
},
|
||||
(error) => {
|
||||
bitmap.clear()
|
||||
reject(error)
|
||||
})
|
||||
}, (error) => {
|
||||
bitmap.clear()
|
||||
reject(error)
|
||||
})
|
||||
// #endif
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
export function sleep(delay) {
|
||||
return new Promise(resolve => setTimeout(resolve, delay))
|
||||
}
|
||||
|
||||
export function getRect(selector, options = {}) {
|
||||
const typeDefault = 'boundingClientRect'
|
||||
const { context, type = typeDefault} = options
|
||||
return new Promise((resolve, reject) => {
|
||||
const dom = uni.createSelectorQuery().in(context).select(selector);
|
||||
const result = (rect) => {
|
||||
if(rect) {
|
||||
resolve(rect)
|
||||
} else {
|
||||
reject()
|
||||
}
|
||||
}
|
||||
if(type == typeDefault) {
|
||||
dom[type](result).exec()
|
||||
} else {
|
||||
dom[type]({
|
||||
node: true,
|
||||
size: true,
|
||||
rect: true
|
||||
}, result).exec()
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export function isTransparent(color) {
|
||||
// 判断颜色是否为 transparent
|
||||
if (color === 'transparent') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 判断颜色是否为 rgba 的 a 为 0
|
||||
if (color.startsWith('rgba')) {
|
||||
const regex = /\d+(\.\d+)?/g;
|
||||
const matches = color.match(regex);
|
||||
if (matches !== null) {
|
||||
const alpha = parseFloat(matches[3]);
|
||||
if (alpha === 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
261
uni_modules/jp-signature/hybrid/html/index.html
Normal file
261
uni_modules/jp-signature/hybrid/html/index.html
Normal file
@@ -0,0 +1,261 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
||||
<title></title>
|
||||
<style type="text/css">
|
||||
html,
|
||||
body,
|
||||
canvas {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-y: hidden;
|
||||
background-color: transparent;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<canvas id="lime-signature"></canvas>
|
||||
<script type="text/javascript" src="./uni.webview.1.5.3.js"></script>
|
||||
<script type="text/javascript" src="./signature.js"></script>
|
||||
<script>
|
||||
var signature = null;
|
||||
var timer = null;
|
||||
var isStart = false;
|
||||
var options = null
|
||||
console.log = function(...args) {
|
||||
postMessage(args);
|
||||
};
|
||||
// function stringify(key, value) {
|
||||
// if (typeof value === 'object' && value !== null) {
|
||||
// if (cache.indexOf(value) !== -1) {
|
||||
// return;
|
||||
// }
|
||||
// cache.push(value);
|
||||
// }
|
||||
// return value;
|
||||
// };
|
||||
function emit(event, data) {
|
||||
postMessage({
|
||||
event,
|
||||
data: typeof data !== "object" && data !== null ? data : JSON.stringify(data),
|
||||
});
|
||||
// cache = [];
|
||||
}
|
||||
|
||||
function postMessage(data) {
|
||||
uni.postMessage({
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
function update(v = {}) {
|
||||
if (signature) {
|
||||
options = v
|
||||
signature.pen.setOption(v);
|
||||
} else {
|
||||
signature = new Signature.Signature({
|
||||
el: "lime-signature"
|
||||
});
|
||||
canvasEl = signature.canvas.get("el");
|
||||
options = v
|
||||
signature.pen.setOption(v)
|
||||
const width = signature.canvas.get("width");
|
||||
const height = signature.canvas.get("height");
|
||||
|
||||
emit({
|
||||
changeSize: {
|
||||
width,
|
||||
height
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function clear() {
|
||||
signature.clear()
|
||||
}
|
||||
|
||||
function undo() {
|
||||
signature.undo()
|
||||
}
|
||||
|
||||
function redo() {
|
||||
signature.redo()
|
||||
}
|
||||
|
||||
function isEmpty() {
|
||||
const isEmpty = signature.isEmpty()
|
||||
emit({
|
||||
isEmpty
|
||||
});
|
||||
}
|
||||
|
||||
function isTransparent(color) {
|
||||
// 判断颜色是否为 transparent
|
||||
if (color === 'transparent') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 判断颜色是否为 rgba 的 a 为 0
|
||||
if (color.startsWith('rgba')) {
|
||||
const regex = /\d+(\.\d+)?/g;
|
||||
const matches = color.match(regex);
|
||||
if (matches !== null) {
|
||||
const alpha = parseFloat(matches[3]);
|
||||
if (alpha === 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function mask(param) {
|
||||
clearTimeout(timer);
|
||||
let {
|
||||
destWidth = 0, destHeight = 0
|
||||
} = param
|
||||
let width = this.signature.canvas.get('width')
|
||||
let height = this.signature.canvas.get('height')
|
||||
let canvas = document.createElement('canvas')
|
||||
const ctx = canvas.getContext('2d');
|
||||
const pixelRatio = signature.canvas.get('pixelRatio')
|
||||
canvas.width = width * pixelRatio
|
||||
canvas.height = height * pixelRatio
|
||||
|
||||
this.signature.pen.getMaskedImageData((imageData) => {
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
if (destWidth && destHeight) {
|
||||
const _canvas = document.createElement('canvas')
|
||||
_canvas.width = destWidth
|
||||
_canvas.height = destHeight
|
||||
const _context = _canvas.getContext('2d')
|
||||
_context.drawImage(canvas, 0, 0, destWidth, destHeight)
|
||||
canvas.remove()
|
||||
canvas = _canvas
|
||||
}
|
||||
const path = canvas.toDataURL();
|
||||
canvas.remove()
|
||||
if (typeof path == "string") {
|
||||
const index = Math.ceil(path.length / 8);
|
||||
for (var i = 0; i < 8; i++) {
|
||||
if (i == 7) {
|
||||
emit({
|
||||
"success": path.substr(i * index, index)
|
||||
});
|
||||
} else {
|
||||
emit({
|
||||
"file": path.substr(i * index, index)
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.error("canvas no data");
|
||||
emit({
|
||||
"fail": "canvas no data"
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function save(param) {
|
||||
// delete param.success;
|
||||
// delete param.fail;
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
let {
|
||||
fileType = 'png', quality = 1, n, destWidth = 0, destHeight = 0
|
||||
} = param
|
||||
const type = `image/${fileType}`.replace(/jpg/, 'jpeg');
|
||||
const {
|
||||
backgroundColor,
|
||||
landscape,
|
||||
boundingBox
|
||||
} = options
|
||||
const flag = backgroundColor || landscape || boundingBox || destWidth && destHeight
|
||||
let path = canvasEl.toDataURL(!flag && type, !flag && quality)
|
||||
if (flag) {
|
||||
let canvas = document.createElement('canvas')
|
||||
const pixelRatio = signature.canvas.get('pixelRatio')
|
||||
let width = signature.canvas.get('width')
|
||||
let height = signature.canvas.get('height')
|
||||
let x = 0
|
||||
let y = 0
|
||||
|
||||
const next = () => {
|
||||
const size = [width, height]
|
||||
if (landscape) {
|
||||
size.reverse()
|
||||
}
|
||||
canvas.width = size[0] * pixelRatio
|
||||
canvas.height = size[1] * pixelRatio
|
||||
const param = [x, y, width, height, 0, 0, width, height].map(item => item * pixelRatio)
|
||||
const context = canvas.getContext('2d')
|
||||
// context.scale(pixelRatio, pixelRatio)
|
||||
if (landscape) {
|
||||
context.translate(0, width * pixelRatio)
|
||||
context.rotate(-Math.PI / 2)
|
||||
}
|
||||
if (backgroundColor && !isTransparent(backgroundColor)) {
|
||||
context.fillStyle = backgroundColor
|
||||
context.fillRect(0, 0, width * pixelRatio, height * pixelRatio)
|
||||
}
|
||||
const drawImage = () => {
|
||||
|
||||
}
|
||||
// param
|
||||
context.drawImage(signature.canvas.get('el'), ...param)
|
||||
if (destWidth && destHeight) {
|
||||
const _canvas = document.createElement('canvas')
|
||||
_canvas.width = destWidth
|
||||
_canvas.height = destHeight
|
||||
const _context = _canvas.getContext('2d')
|
||||
_context.drawImage(canvas, 0, 0, destWidth, destHeight)
|
||||
canvas.remove()
|
||||
canvas = _canvas
|
||||
}
|
||||
path = canvas.toDataURL(type, quality)
|
||||
canvas.remove()
|
||||
}
|
||||
if (boundingBox) {
|
||||
const res = signature.getContentBoundingBox()
|
||||
width = res.width
|
||||
height = res.height
|
||||
x = res.startX
|
||||
y = res.startY
|
||||
next()
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
}
|
||||
if (typeof path == "string") {
|
||||
const index = Math.ceil(path.length / 8);
|
||||
for (var i = 0; i < 8; i++) {
|
||||
if (i == 7) {
|
||||
emit({
|
||||
"success": path.substr(i * index, index)
|
||||
});
|
||||
} else {
|
||||
emit({
|
||||
"file": path.substr(i * index, index)
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.error("canvas no data");
|
||||
emit({
|
||||
"fail": "canvas no data"
|
||||
});
|
||||
}
|
||||
}, 30);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
1
uni_modules/jp-signature/hybrid/html/signature.js
Normal file
1
uni_modules/jp-signature/hybrid/html/signature.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
78
uni_modules/jp-signature/package.json
Normal file
78
uni_modules/jp-signature/package.json
Normal file
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"id": "jp-signature",
|
||||
"displayName": "手写签名组件,弹框签名,可配置签名,签名返回base64,签名专用,手写一键使用 文档签字",
|
||||
"version": "3.2.0",
|
||||
"description": "用于手写签名,同时内置了弹框签名组件及文档上签字组件,对于不想布局的同学来说可以开箱即用。",
|
||||
"keywords": [
|
||||
"手写签名",
|
||||
"弹框签名",
|
||||
"手写一键使用",
|
||||
"文档签字",
|
||||
"小白专用签名"
|
||||
],
|
||||
"repository": "",
|
||||
"engines": {
|
||||
"HBuilderX": "^3.7.8"
|
||||
},
|
||||
"dcloudext": {
|
||||
"sale": {
|
||||
"regular": {
|
||||
"price": "0.00"
|
||||
},
|
||||
"sourcecode": {
|
||||
"price": "0.00"
|
||||
}
|
||||
},
|
||||
"contact": {
|
||||
"qq": ""
|
||||
},
|
||||
"declaration": {
|
||||
"ads": "无",
|
||||
"data": "无",
|
||||
"permissions": "无"
|
||||
},
|
||||
"npmurl": "",
|
||||
"type": "component-vue"
|
||||
},
|
||||
"uni_modules": {
|
||||
"dependencies": [],
|
||||
"encrypt": [],
|
||||
"platforms": {
|
||||
"cloud": {
|
||||
"tcb": "y",
|
||||
"aliyun": "y",
|
||||
"alipay": "n"
|
||||
},
|
||||
"client": {
|
||||
"App": {
|
||||
"app-vue": "y",
|
||||
"app-nvue": "y"
|
||||
},
|
||||
"H5-mobile": {
|
||||
"Safari": "y",
|
||||
"Android Browser": "y",
|
||||
"微信浏览器(Android)": "y",
|
||||
"QQ浏览器(Android)": "y"
|
||||
},
|
||||
"H5-pc": {
|
||||
"Chrome": "n",
|
||||
"IE": "n",
|
||||
"Edge": "n",
|
||||
"Firefox": "n",
|
||||
"Safari": "n"
|
||||
},
|
||||
"小程序": {
|
||||
"微信": "y",
|
||||
"阿里": "u",
|
||||
"百度": "u",
|
||||
"字节跳动": "u",
|
||||
"QQ": "u"
|
||||
},
|
||||
"快应用": {
|
||||
"华为": "u",
|
||||
"联盟": "u"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
214
uni_modules/jp-signature/readme.md
Normal file
214
uni_modules/jp-signature/readme.md
Normal file
@@ -0,0 +1,214 @@
|
||||
## jp-signature 、jp-signature-popup、 jp-merge 写字板
|
||||
### jp-signature 写字板,可用业务签名等场景,方便用户自行改造
|
||||
### jp-signature-popup 小白专用弹框签名组件,方便小白开发使用,和输入框一样使用简单
|
||||
### jp-merge 图片签字组件,提供用户在图片文档上进行签字
|
||||
|
||||
# 有合作需求请私
|
||||
## 开发不易,如果帮助到你的,请支持 有问题请留言,作者会积极更新
|
||||
|
||||
## 平台兼容
|
||||
| H5 | 微信小程序 | 支付宝小程序 | 百度小程序 | 头条小程序 | QQ 小程序 | App |
|
||||
| --- | ---------- | ------------ | ---------- | ---------- | --------- | --- |
|
||||
| √ | √ | √ | 未测 | 未测 | 未测 | √ |
|
||||
|
||||
|
||||
## 代码演示
|
||||
|
||||
### jp-signature 基本用法
|
||||
```html
|
||||
<view style="width: 750rpx ;height: 500rpx;">
|
||||
<jp-signature ref="signatureRef" ></jp-signature>
|
||||
</view>
|
||||
<view>
|
||||
<button @click="clear">清空</button>
|
||||
<button @click="">撤消</button>
|
||||
<button @click="save">保存</button>
|
||||
</view>
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
url: '',
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
save(){
|
||||
this.$refs.signatureRef.canvasToTempFilePath({
|
||||
success: (res) => {
|
||||
// 是否为空画板 无签名
|
||||
console.log(res.isEmpty)
|
||||
// 生成图片的临时路径
|
||||
// H5 生成的是base64
|
||||
this.url = res.tempFilePath
|
||||
}
|
||||
})
|
||||
},
|
||||
clear(){
|
||||
this.$refs.signatureRef.clear()
|
||||
},
|
||||
undo(){
|
||||
this.$refs.signatureRef.undo()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## API
|
||||
### Props
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
| -------------- | ------------ | ---------------- | ------------ |
|
||||
| penSize | 画笔大小 | <em>number</em> | `2` |
|
||||
| minLineWidth | 线条最小宽 | <em>number</em> | `2` |
|
||||
| maxLineWidth | 线条最大宽 | <em>number</em> | `6` |
|
||||
| penColor | 画笔颜色 | <em>string</em> | `black` |
|
||||
| backgroundColor | 背景颜色 | <em>string</em> | `` |
|
||||
| type | 指定 canvas 类型 | <em>string</em> | `2d` |
|
||||
| openSmooth | 是否模拟压感 | <em>boolean</em> | `false` |
|
||||
| beforeDelay | 延时初始化,在放在弹窗里可以使用 (毫秒) | <em>number</em> | `0` |
|
||||
| maxHistoryLength | 限制历史记录数,即最大可撤销数,传入0则关闭历史记录功能 | <em>boolean</em> | `20` |
|
||||
| landscape | 横屏 | <em>boolean</em> | `` |
|
||||
| disableScroll | 当在写字时,禁止屏幕滚动以及下拉刷新 | <em>boolean</em> | `true` |
|
||||
| boundingBox | 只生成内容区域,即未画部分不生成,有性能的损耗(微信小程序pc不支持) | <em>boolean</em> | `false` |
|
||||
|
||||
|
||||
### 事件 Events
|
||||
|
||||
| 事件名 | 说明 | 回调 |
|
||||
| ------- | ------------ | -------------- |
|
||||
| undo | 撤消,回退到上一步 | |
|
||||
| clear | 清空,清空画板 | |
|
||||
| canvasToTempFilePath | 保存,生成图片,与官方保持一致,但不需要传canvasId | |
|
||||
|
||||
### 常见问题
|
||||
- 放在弹窗里时,尺寸不对 可以延时手写板出现时机,给手写板加vif或beforeDelay="300"
|
||||
- boundingBox 微信小程序 pc 不支持, 因为获取不到 ImageData 数据
|
||||
|
||||
|
||||
|
||||
## jp-signature-popup 基础用法
|
||||
```html
|
||||
<template>
|
||||
<view class="content">
|
||||
<!-- #ifdef VUE2 -->
|
||||
<jp-signature-popup v-model="title" />
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef VUE3 -->
|
||||
<jp-signature-popup v-model:value="title" />
|
||||
<!-- #endif -->
|
||||
{{title}}
|
||||
</view>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
title: ''
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
####参数
|
||||
| 参数名 | 类型 | 默认值 | 说明 |
|
||||
| -------- | -------- | --------| --------|
|
||||
| value | String | | 签名内容,可以通过v-model和v-model:value绑定 |
|
||||
| label | String | 手写签名 | |
|
||||
| popup | Boolean | false | 是否隐藏原有样式,该模式只使用弹框 |
|
||||
| required | Boolean | false | |
|
||||
| placeholder | String | 点击签名 | 签名说明 |
|
||||
| readonly | Boolean | false | 是否只能可读 |
|
||||
| openSmooth | Boolean | false | 是否开启签名笔锋 |
|
||||
| boundingBox | boolean| false | 只生成内容区域,即未画部分不生成,有性能的损耗(微信小程序不支持) |
|
||||
|
||||
|
||||
####方法
|
||||
| 方法名 | 返回参数 | 说明 |
|
||||
| -------- | -------- | --------|
|
||||
| toPop | | 打开弹窗 |
|
||||
| close | | 关闭弹窗 |
|
||||
| deleteImg | | 删除内容 |
|
||||
|
||||
####事假
|
||||
| 事件名 | 返回参数 | 说明 |
|
||||
| -------- | -------- | --------|
|
||||
| input | 签名内容 | 签名内容 |
|
||||
| change | 签名内容 | 签名内容改变后触发 |
|
||||
| toImg | 图片编码 | 点击图片时触发 |
|
||||
|
||||
|
||||
## jp-merge 基础用法
|
||||
```html
|
||||
<template>
|
||||
<view class="content">
|
||||
<view>下面是使用 jp-signature-popup 结合 jp-merge 在文档上签字</view>
|
||||
<view style="text-align: center;padding-bottom: 150px;">
|
||||
<image :src="image4" v-if="image4" style="width: 350px;height: 350px;border: 1px solid #ccc;"></image>
|
||||
<image src="../../static/sqs.jpg" v-else style="width: 350px;height: 350px;border: 1px solid #ccc;"></image>
|
||||
<view class="but" style="margin: 0 25px;" @click="toPop">我要在上面签字</view>
|
||||
<jp-signature-popup ref="signature" @change="setImg" popup v-model:value="image3" />
|
||||
<jp-merge bgImage="../../static/sqs.jpg" ref="jpMerge"></jp-merge>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
image3:'',
|
||||
image4:''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setImg(val){
|
||||
if(val){
|
||||
<!-- 生成签字结果的方法可以传入网络及本地图片 -->
|
||||
this.$refs.jpMerge.exportPost(val).then(res => {
|
||||
this.image4 = res
|
||||
})
|
||||
}
|
||||
},
|
||||
toPop(){
|
||||
this.$refs.signature.toPop()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.but{
|
||||
margin: 25px;
|
||||
line-height: 40px;
|
||||
text-align: center;
|
||||
background-color: #55aaff;
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
####参数
|
||||
| 参数名 | 类型 | 默认值 | 说明 |
|
||||
| -------- | -------- | --------| --------|
|
||||
| bgImage | String | | 文档图片地址,支持本地及线上图片(小程序如果是网络图片需要配置白名单) |
|
||||
| canvasWidth | number | 400 | 生成图片的最终宽,建议长高和文档长高一致 |
|
||||
| canvasHeight | number | 400 | 生成图片的最终高,建议长高和文档长高一致 |
|
||||
| width | number | 80 | 签字图片宽 |
|
||||
| height | number | 80 | 签字图片高 |
|
||||
| left | number | 80 | 签字图片距离左边位置 |
|
||||
| top | number | 80 | 签字图片距离顶边位置 |
|
||||
|
||||
|
||||
####方法
|
||||
| 方法名 | 返回参数 | 说明 |
|
||||
| -------- | -------- | --------|
|
||||
| exportPost | | 传入签字图片生成最终结果 |
|
||||
|
||||
|
||||
|
||||
### 常见问题
|
||||
- 在vue2和vue3中使用v-model有区别,vue2中为v-model,vue3为v-model:value
|
||||
- 使用实例已放在 uni_modules/jp-signature/pages 中,可复制后测试
|
||||
|
||||
|
||||
|
||||
70
uni_modules/jp-signature/实例/index.vue
Normal file
70
uni_modules/jp-signature/实例/index.vue
Normal file
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<view class="h100">
|
||||
<!-- #ifdef VUE2 -->
|
||||
<jp-signature-popup v-model="image" required />
|
||||
<view>{{image}}</view>
|
||||
<view class="but" @click="toPop1">自定义样式,弹框调用</view>
|
||||
<jp-signature-popup ref="signature1" popup v-model="image2" />
|
||||
<image :src="image2" style="width: 200px;" mode="widthFix"></image>
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef VUE3 -->
|
||||
<jp-signature-popup v-model:value="image" required />
|
||||
<view>{{image}}</view>
|
||||
<view class="but" @click="toPop1">自定义样式,弹框调用</view>
|
||||
<jp-signature-popup ref="signature1" popup v-model:value="image2" />
|
||||
<image :src="image2" style="width: 200px;" mode="widthFix"></image>
|
||||
<!-- #endif -->
|
||||
{{image2}}
|
||||
|
||||
|
||||
<view>下面是使用 jp-signature-popup 结合 jp-merge 在文档上签字</view>
|
||||
<view style="text-align: center;padding-bottom: 150px;">
|
||||
<image :src="image4" v-if="image4" style="width: 350px;height: 350px;border: 1px solid #ccc;"></image>
|
||||
<image src="../../static/sqs.jpg" v-else style="width: 350px;height: 350px;border: 1px solid #ccc;"></image>
|
||||
<view class="but" style="margin: 0 25px;" @click="toPop2">我要在上面签字</view>
|
||||
<jp-signature-popup ref="signature2" @change="setImg" popup v-model:value="image3" />
|
||||
<jp-merge bgImage="../../static/sqs.jpg" ref="jpMerge"></jp-merge>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<!-- 有项目需要开发的请联系 扣 - 371524845 -->
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
image:'',
|
||||
image2:'',
|
||||
image3:'',
|
||||
image4:''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setImg(val){
|
||||
if(val){
|
||||
this.$refs.jpMerge.exportPost(val).then(res => {
|
||||
this.image4 = res
|
||||
})
|
||||
}
|
||||
},
|
||||
toPop1(){
|
||||
this.$refs.signature1.toPop()
|
||||
},
|
||||
toPop2(){
|
||||
this.$refs.signature2.toPop()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.but{
|
||||
margin: 25px;
|
||||
line-height: 40px;
|
||||
text-align: center;
|
||||
background-color: #55aaff;
|
||||
color: #fff;
|
||||
}
|
||||
.sssv{
|
||||
height: 1150px;
|
||||
}
|
||||
</style>
|
||||
108
uni_modules/jp-signature/实例/微信小程序签名到指定位置.vue
Normal file
108
uni_modules/jp-signature/实例/微信小程序签名到指定位置.vue
Normal file
@@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<view class="h100">
|
||||
<view style="text-align: center;padding-bottom: 150px;">
|
||||
<image :src="image" v-if="image" style="width: 200px;height: 200px;border: 1px solid #ccc;"></image>
|
||||
<image :src="imgs" v-else style="width: 200px;height: 200px;border: 1px solid #ccc;"></image>
|
||||
<view class="but" style="margin: 0 25px;" @click="toPop">我要在上面签字</view>
|
||||
<jp-signature-popup ref="signature2" @change="setImg" popup />
|
||||
<canvas canvas-id="shareCanvas" class="canvas" bindlongpress="saveImg" catchtouchmove="true"
|
||||
style="position:fixed;left:500%"
|
||||
:style="{height: canvasHeight+'px',width:canvasWidth+'px'}">
|
||||
</canvas>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<!-- 有项目需要开发的请联系 扣 - 371524845 -->
|
||||
<!--
|
||||
注意:需要采用线上图片且需要配置白名单,未配置手机无法签名,采用真机调试2.0不配置白名单也可以签名,正式版本需要线上图片且需要配置白名单
|
||||
-->
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
canvasHeight: 400,
|
||||
canvasWidth: 400,
|
||||
width:80,
|
||||
height: 50,
|
||||
left: 20,
|
||||
top: 20,
|
||||
ctx:null,
|
||||
image: '',
|
||||
imgs: 'http://mmbiz.qpic.cn/sz_mmbiz_jpg/GEWVeJPFkSGTfkSpSbg9cHUqcibBv38r8GXDIVy4W6FN7a1TMWf6RSNQLemKBwG8VqjlxUhicIzz3NTONVrD96ibg/0?wx_fmt=jpeg'
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
//初始化画布
|
||||
this.ctx = wx.createCanvasContext('shareCanvas', this)
|
||||
},
|
||||
methods: {
|
||||
setImg(val) {
|
||||
if (val) {
|
||||
this.exportPost(val).then(res => {
|
||||
this.image = res
|
||||
})
|
||||
}
|
||||
},
|
||||
toPop() {
|
||||
this.$refs.signature2.toPop()
|
||||
},
|
||||
getImageInfo(src) {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.getImageInfo({
|
||||
src,
|
||||
success: (res) => resolve(res),
|
||||
fail: (res) => reject(res)
|
||||
})
|
||||
});
|
||||
},
|
||||
exportPost(image2){
|
||||
let that = this
|
||||
return new Promise(function (resolve, reject) {
|
||||
let image = that.imgs
|
||||
//获取系统的基本信息,为后期的画布和底图适配宽高
|
||||
uni.getSystemInfo({
|
||||
success: function (res) {
|
||||
Promise.all([that.getImageInfo(image),that.getImageInfo(image2)]).then(res=>{
|
||||
//获取底图和二维码图片的基本信息,通常前端导出的二维码是base64格式的,所以要转成图片格式的才可以获取图片的基本信息
|
||||
that.ctx.drawImage(res[0].path,0 , 0,that.canvasWidth,that.canvasHeight);
|
||||
that.ctx.drawImage(res[1].path,that.left,that.top,that.width, that.height);
|
||||
that.ctx.draw(false,function(){
|
||||
wx.canvasToTempFilePath({
|
||||
x: 0,
|
||||
y: 0,
|
||||
width:that.canvasWidth,
|
||||
height:that.canvasHeight,
|
||||
destWidth:that.canvasWidth*2,//这里乘以2是为了保证合成图片的清晰度
|
||||
destHeight:that.canvasHeight*2,
|
||||
canvasId: 'shareCanvas',
|
||||
fileType:'jpg',//设置导出图片的后缀名
|
||||
success: function (res) {
|
||||
resolve(res.tempFilePath)
|
||||
},
|
||||
fail: function (res) {
|
||||
reject(res)
|
||||
},
|
||||
})
|
||||
});
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.but {
|
||||
margin: 25px;
|
||||
line-height: 40px;
|
||||
text-align: center;
|
||||
background-color: #55aaff;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.sssv {
|
||||
height: 1150px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user